diff --git a/ASSET-LICENSE.md b/ASSET-LICENSE.md index c438995..9519836 100644 --- a/ASSET-LICENSE.md +++ b/ASSET-LICENSE.md @@ -1,15 +1,15 @@ -# NETfishing Asset License 1.1 +# 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 NETfishing artwork, graphics, textures, +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 NETfishing owners +designs, characters, and other creative assets owned by the straywild owners or licensed to them with authority to sublicense, publishing as Woofmeow (the -**“NETfishing Assets”**). +**“straywild Assets”**). It does not apply to: @@ -17,77 +17,77 @@ It does not apply to: - 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 NETfishing and Woofmeow names and logos, which are addressed in +- 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 NETfishing Software** means: +**Official straywild Software** means: -- an unmodified binary release published by the NETfishing owners through an - official NETfishing distribution channel; or -- a build made from an unmodified official NETfishing source release or tagged +- 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 -NETfishing Software. A Mod may add, replace, or alter content loaded by the -Official NETfishing Software, but it may not redistribute a replacement copy +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 -NETfishing source repository that is created and maintained in good faith +straywild source repository that is created and maintained in good faith primarily to prepare, test, review, or submit changes for possible inclusion -in Official NETfishing Software. It does not include a fork presented or +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 NETfishing project. +or ongoing product separate from the official straywild project. ## 3. Permission to use the official game -You may reproduce NETfishing Assets as technically necessary to install, run, -back up, and privately modify a lawfully obtained copy of Official NETfishing +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 NETfishing. This permission does not authorize redistribution +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 NETfishing -Assets solely as part of a Mod for Official NETfishing Software. +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 -NETfishing Assets reasonably necessary for the Mod to function. It does not +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 NETfishing Software; +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 NETfishing assets © 2026 Woofmeow, used under the - > NETfishing Asset License 1.1. This Mod is unofficial and is not endorsed + > 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 NETfishing +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 NETfishing +charging for access to it, licensing it commercially, or using straywild Assets in paid products requires separate written permission from both -NETfishing owners. +straywild owners. ## 5. Contribution Fork license -Subject to this license, you may reproduce, modify, and distribute NETfishing +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. @@ -99,8 +99,8 @@ fork are permitted distributions. A Contribution Fork must: 1. remain primarily directed toward preparing changes for possible inclusion - in the official NETfishing project; -2. preserve this license and the official NETfishing project's applicable + 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; @@ -109,63 +109,63 @@ A Contribution Fork must: contribution; 5. not publish general-audience binaries, releases, packages, or downloads, operate a persistent public alternative service, or monetize access; -6. not distribute the NETfishing Assets as a standalone extraction, archive, +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 NETfishing Software or its services. +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 NETfishing owners to accept the +`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 NETfishing Assets. +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 NETfishing owners, you may not use or distribute -NETfishing Assets: +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 NETfishing code that is not - Official NETfishing Software, except for nonpublic contribution-testing +- 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 NETfishing Assets themselves. +- 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 NETfishing Assets. A code fork that is not a -Contribution Fork must remove or replace the NETfishing Assets and reserved +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 NETfishing owners retain ownership of the -NETfishing Assets and adaptations of those assets to the extent provided by +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 NETfishing Assets. If -you intentionally submit material for inclusion in NETfishing, +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 NETfishing owners do not notify you of the violation +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 @@ -173,15 +173,15 @@ license. ## 9. No warranty -THE NETFISHING ASSETS ARE PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, +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 NETFISHING ASSETS. +OTHER LIABILITY ARISING FROM THIS LICENSE OR USE OF THE straywild ASSETS. ## 10. Additional permission -Uses not granted here require written permission from both NETfishing owners. +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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7017110..2dc5724 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contribution policy -NETfishing is an owner-directed Woofmeow project and is not accepting +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 @@ -17,7 +17,7 @@ work; it is not a standing invitation to contribute. ## Contribution forks -The [NETfishing Asset License](ASSET-LICENSE.md) contains limited permissions +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 @@ -79,4 +79,4 @@ 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 NETfishing Asset License. +identifier to asset files governed by the straywild Asset License. diff --git a/CONTRIBUTOR-TERMS.md b/CONTRIBUTOR-TERMS.md index e0d801a..261de60 100644 --- a/CONTRIBUTOR-TERMS.md +++ b/CONTRIBUTOR-TERMS.md @@ -1,6 +1,6 @@ -# NETfishing contributor terms +# straywild contributor terms -NETfishing does not accept unsolicited contributions. These terms are retained +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 @@ -9,9 +9,9 @@ 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 NETfishing co-owners who publish the Project as +“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 NETfishing game, its servers, tools, documentation, websites, +the official straywild game, its servers, tools, documentation, websites, ports, updates, and promotional materials. ## 1. Your ownership and authority @@ -56,8 +56,8 @@ royalty-free, transferable, and sublicensable license to: - 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 NETfishing, its promotion, and official successor or -derived NETfishing products. It does not transfer your copyright or authorize +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 @@ -78,7 +78,7 @@ or release. ## 6. No automatic ownership, employment, or payment -Submitting a contribution does not provide ownership of NETfishing, create an +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. diff --git a/README.md b/README.md index 132b3f8..e228e7b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# NETfishing +# straywild -NETfishing is a multiplayer-first fishing game built with Godot 4.7. The +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. @@ -8,39 +8,39 @@ 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. -NETfishing is developed and published by **Woofmeow**, the independent team of +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/NETfishing/ +Steam: https://store.steampowered.com/app/5068950/straywild/ -Itch.io: https://sol6-vi.itch.io/netfishing +Itch.io: https://sol6-vi.itch.io/straywild Discord: https://discord.gg/5gP22447kc -Matrix: https://matrix.to/#/#netfishing:matrix.makearmy.io +Matrix: https://matrix.to/#/#straywild:matrix.makearmy.io -Repo: https://forge.makearmy.io/woofmeow/netfishing +Repo: https://forge.makearmy.io/woofmeow/straywild -Releases: https://forge.makearmy.io/woofmeow/netfishing/releases +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 NETfishing from the extracted platform folder. +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 NETfishing +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 NETfishing build downloaded from an official +Only bypass this warning for a straywild build downloaded from an official project release. ## Requirements @@ -110,10 +110,10 @@ world and UI authoring, and testing are consolidated in - 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 [NETfishing Asset License](ASSET-LICENSE.md). It permits community - Mods for official NETfishing software and narrowly scoped contribution forks, + 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 NETfishing and Woofmeow names and branding are reserved as described in +- 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). diff --git a/TRADEMARKS.md b/TRADEMARKS.md index 363248b..fbf49d5 100644 --- a/TRADEMARKS.md +++ b/TRADEMARKS.md @@ -1,7 +1,7 @@ -# NETfishing and Woofmeow branding +# straywild and Woofmeow branding -The NETfishing and Woofmeow names, logos, icons, trade dress, and other source -identifiers are reserved by the NETfishing owners, publishing as Woofmeow. +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. @@ -10,30 +10,30 @@ exist through use and other law. ## Permitted references -You may use the names NETfishing and Woofmeow truthfully to: +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 NETfishing; +- 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 NETfishing” if its +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 -NETfishing and Woofmeow names, logos, icons, and trade dress already present in +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 NETfishing or +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. @@ -44,7 +44,7 @@ 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 NETfishing and Woofmeow logos, icons, and official trade dress; +- 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. @@ -53,4 +53,4 @@ 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 -NETfishing owners. +straywild owners. diff --git a/addons/netfishing_shoreline_baker/plugin.cfg b/addons/straywild_shoreline_baker/plugin.cfg similarity index 81% rename from addons/netfishing_shoreline_baker/plugin.cfg rename to addons/straywild_shoreline_baker/plugin.cfg index c8c4d06..c3f4e47 100644 --- a/addons/netfishing_shoreline_baker/plugin.cfg +++ b/addons/straywild_shoreline_baker/plugin.cfg @@ -1,6 +1,6 @@ [plugin] -name="NETfishing Shoreline Auto-Baker" +name="straywild Shoreline Auto-Baker" description="Rebuilds configured static shoreline ribbons after authored terrain reimports." author="woofmeow" version="1.0.0" diff --git a/addons/netfishing_shoreline_baker/shoreline_auto_baker.gd b/addons/straywild_shoreline_baker/shoreline_auto_baker.gd similarity index 100% rename from addons/netfishing_shoreline_baker/shoreline_auto_baker.gd rename to addons/straywild_shoreline_baker/shoreline_auto_baker.gd diff --git a/addons/netfishing_shoreline_baker/shoreline_auto_baker.gd.uid b/addons/straywild_shoreline_baker/shoreline_auto_baker.gd.uid similarity index 100% rename from addons/netfishing_shoreline_baker/shoreline_auto_baker.gd.uid rename to addons/straywild_shoreline_baker/shoreline_auto_baker.gd.uid diff --git a/art/exported/characters/base/netfishing_base_character.glb b/art/exported/characters/base/straywild_base_character.glb similarity index 100% rename from art/exported/characters/base/netfishing_base_character.glb rename to art/exported/characters/base/straywild_base_character.glb diff --git a/art/exported/characters/base/netfishing_base_character.glb.import b/art/exported/characters/base/straywild_base_character.glb.import similarity index 77% rename from art/exported/characters/base/netfishing_base_character.glb.import rename to art/exported/characters/base/straywild_base_character.glb.import index b1409e1..96925de 100644 --- a/art/exported/characters/base/netfishing_base_character.glb.import +++ b/art/exported/characters/base/straywild_base_character.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://w7n4gjqq1juc" -path="res://.godot/imported/netfishing_base_character.glb-0167c087f16181d405afb962dc877d2c.scn" +path="res://.godot/imported/straywild_base_character.glb-65021653e51a906d3b4a3494f170317f.scn" [deps] -source_file="res://art/exported/characters/base/netfishing_base_character.glb" -dest_files=["res://.godot/imported/netfishing_base_character.glb-0167c087f16181d405afb962dc877d2c.scn"] +source_file="res://art/exported/characters/base/straywild_base_character.glb" +dest_files=["res://.godot/imported/straywild_base_character.glb-65021653e51a906d3b4a3494f170317f.scn"] [params] diff --git a/art/exported/characters/base/netfishing_base_character_eyes_simple_shine.png b/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png similarity index 100% rename from art/exported/characters/base/netfishing_base_character_eyes_simple_shine.png rename to art/exported/characters/base/straywild_base_character_eyes_simple_shine.png diff --git a/art/exported/characters/base/netfishing_base_character_eyes_simple_shine.png.import b/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png.import similarity index 58% rename from art/exported/characters/base/netfishing_base_character_eyes_simple_shine.png.import rename to art/exported/characters/base/straywild_base_character_eyes_simple_shine.png.import index 4141bca..ddf8bb7 100644 --- a/art/exported/characters/base/netfishing_base_character_eyes_simple_shine.png.import +++ b/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://dyjwgkgw0syo8" -path.s3tc="res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/exported/characters/base/netfishing_base_character_eyes_simple_shine.png" -dest_files=["res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.s3tc.ctex", "res://.godot/imported/netfishing_base_character_eyes_simple_shine.png-140bfa3aa0b6f662e8ff9e5721213a61.etc2.ctex"] +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] diff --git a/art/exported/characters/base/netfishing_base_character_mouth_three.png b/art/exported/characters/base/straywild_base_character_mouth_three.png similarity index 100% rename from art/exported/characters/base/netfishing_base_character_mouth_three.png rename to art/exported/characters/base/straywild_base_character_mouth_three.png diff --git a/art/exported/characters/base/netfishing_base_character_mouth_three.png.import b/art/exported/characters/base/straywild_base_character_mouth_three.png.import similarity index 59% rename from art/exported/characters/base/netfishing_base_character_mouth_three.png.import rename to art/exported/characters/base/straywild_base_character_mouth_three.png.import index 10d0721..ba0ec2c 100644 --- a/art/exported/characters/base/netfishing_base_character_mouth_three.png.import +++ b/art/exported/characters/base/straywild_base_character_mouth_three.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://dt5gitdtjdvcd" -path.s3tc="res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/exported/characters/base/netfishing_base_character_mouth_three.png" -dest_files=["res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.s3tc.ctex", "res://.godot/imported/netfishing_base_character_mouth_three.png-90b4a98403d2a291a56a8ed42aaa8473.etc2.ctex"] +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] diff --git a/art/exported/characters/base/netfishing_base_character_nose_dog_round.png b/art/exported/characters/base/straywild_base_character_nose_dog_round.png similarity index 100% rename from art/exported/characters/base/netfishing_base_character_nose_dog_round.png rename to art/exported/characters/base/straywild_base_character_nose_dog_round.png diff --git a/art/exported/characters/base/netfishing_base_character_nose_dog_round.png.import b/art/exported/characters/base/straywild_base_character_nose_dog_round.png.import similarity index 59% rename from art/exported/characters/base/netfishing_base_character_nose_dog_round.png.import rename to art/exported/characters/base/straywild_base_character_nose_dog_round.png.import index 71d25b6..cb2c573 100644 --- a/art/exported/characters/base/netfishing_base_character_nose_dog_round.png.import +++ b/art/exported/characters/base/straywild_base_character_nose_dog_round.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://bmho1ryst3smt" -path.s3tc="res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/exported/characters/base/netfishing_base_character_nose_dog_round.png" -dest_files=["res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.s3tc.ctex", "res://.godot/imported/netfishing_base_character_nose_dog_round.png-0008adff5271dd0403e610ef13dd52cf.etc2.ctex"] +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] diff --git a/art/exported/creatures/2502.png b/art/exported/creatures/2502.png new file mode 100644 index 0000000..467431b Binary files /dev/null and b/art/exported/creatures/2502.png differ diff --git a/art/exported/creatures/2502.png.import b/art/exported/creatures/2502.png.import new file mode 100644 index 0000000..17bcad9 --- /dev/null +++ b/art/exported/creatures/2502.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://busj0feq2dncu" +path.s3tc="res://.godot/imported/2502.png-3922e306611b91541a86520cf32cbdbd.s3tc.ctex" +path.etc2="res://.godot/imported/2502.png-3922e306611b91541a86520cf32cbdbd.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2502.png" +dest_files=["res://.godot/imported/2502.png-3922e306611b91541a86520cf32cbdbd.s3tc.ctex", "res://.godot/imported/2502.png-3922e306611b91541a86520cf32cbdbd.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/2504.png b/art/exported/creatures/2504.png new file mode 100644 index 0000000..d0279d8 Binary files /dev/null and b/art/exported/creatures/2504.png differ diff --git a/art/exported/creatures/2504.png.import b/art/exported/creatures/2504.png.import new file mode 100644 index 0000000..73fa801 --- /dev/null +++ b/art/exported/creatures/2504.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://4a7c1aat4777" +path.s3tc="res://.godot/imported/2504.png-c9f47ab152b933d8158b8c058ec5fcc2.s3tc.ctex" +path.etc2="res://.godot/imported/2504.png-c9f47ab152b933d8158b8c058ec5fcc2.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2504.png" +dest_files=["res://.godot/imported/2504.png-c9f47ab152b933d8158b8c058ec5fcc2.s3tc.ctex", "res://.godot/imported/2504.png-c9f47ab152b933d8158b8c058ec5fcc2.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/2505.png b/art/exported/creatures/2505.png new file mode 100644 index 0000000..7463c25 Binary files /dev/null and b/art/exported/creatures/2505.png differ diff --git a/art/exported/creatures/2505.png.import b/art/exported/creatures/2505.png.import new file mode 100644 index 0000000..130cedb --- /dev/null +++ b/art/exported/creatures/2505.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b8u3lw242yr3c" +path.s3tc="res://.godot/imported/2505.png-fe14d70a6372dbe0cc4a8275d0816cad.s3tc.ctex" +path.etc2="res://.godot/imported/2505.png-fe14d70a6372dbe0cc4a8275d0816cad.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2505.png" +dest_files=["res://.godot/imported/2505.png-fe14d70a6372dbe0cc4a8275d0816cad.s3tc.ctex", "res://.godot/imported/2505.png-fe14d70a6372dbe0cc4a8275d0816cad.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/2508.png b/art/exported/creatures/2508.png new file mode 100644 index 0000000..4cefefd Binary files /dev/null and b/art/exported/creatures/2508.png differ diff --git a/art/exported/creatures/2508.png.import b/art/exported/creatures/2508.png.import new file mode 100644 index 0000000..0fdbbe4 --- /dev/null +++ b/art/exported/creatures/2508.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c2k6ghkng6081" +path.s3tc="res://.godot/imported/2508.png-0478df4a4cb2af8e08ada8086efda0c3.s3tc.ctex" +path.etc2="res://.godot/imported/2508.png-0478df4a4cb2af8e08ada8086efda0c3.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2508.png" +dest_files=["res://.godot/imported/2508.png-0478df4a4cb2af8e08ada8086efda0c3.s3tc.ctex", "res://.godot/imported/2508.png-0478df4a4cb2af8e08ada8086efda0c3.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/2601.png b/art/exported/creatures/2601.png new file mode 100644 index 0000000..f7565d8 Binary files /dev/null and b/art/exported/creatures/2601.png differ diff --git a/art/exported/creatures/2601.png.import b/art/exported/creatures/2601.png.import new file mode 100644 index 0000000..7444643 --- /dev/null +++ b/art/exported/creatures/2601.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d14jvhurcfryb" +path.s3tc="res://.godot/imported/2601.png-912942ea4c6b2c66982e089ee46fde12.s3tc.ctex" +path.etc2="res://.godot/imported/2601.png-912942ea4c6b2c66982e089ee46fde12.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2601.png" +dest_files=["res://.godot/imported/2601.png-912942ea4c6b2c66982e089ee46fde12.s3tc.ctex", "res://.godot/imported/2601.png-912942ea4c6b2c66982e089ee46fde12.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/2801.png b/art/exported/creatures/2801.png new file mode 100644 index 0000000..ad8294d Binary files /dev/null and b/art/exported/creatures/2801.png differ diff --git a/art/exported/creatures/2801.png.import b/art/exported/creatures/2801.png.import new file mode 100644 index 0000000..512f812 --- /dev/null +++ b/art/exported/creatures/2801.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cc7el34sfad78" +path.s3tc="res://.godot/imported/2801.png-d1c5df9faa2d1eb1cc3453c8e18d4049.s3tc.ctex" +path.etc2="res://.godot/imported/2801.png-d1c5df9faa2d1eb1cc3453c8e18d4049.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2801.png" +dest_files=["res://.godot/imported/2801.png-d1c5df9faa2d1eb1cc3453c8e18d4049.s3tc.ctex", "res://.godot/imported/2801.png-d1c5df9faa2d1eb1cc3453c8e18d4049.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/2802.png b/art/exported/creatures/2802.png new file mode 100644 index 0000000..4190610 Binary files /dev/null and b/art/exported/creatures/2802.png differ diff --git a/art/exported/creatures/2802.png.import b/art/exported/creatures/2802.png.import new file mode 100644 index 0000000..4d014d1 --- /dev/null +++ b/art/exported/creatures/2802.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bawecr4tv52p1" +path.s3tc="res://.godot/imported/2802.png-6319ce264fbd950e232fd0b428319265.s3tc.ctex" +path.etc2="res://.godot/imported/2802.png-6319ce264fbd950e232fd0b428319265.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2802.png" +dest_files=["res://.godot/imported/2802.png-6319ce264fbd950e232fd0b428319265.s3tc.ctex", "res://.godot/imported/2802.png-6319ce264fbd950e232fd0b428319265.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/2803.png b/art/exported/creatures/2803.png new file mode 100644 index 0000000..6360f26 Binary files /dev/null and b/art/exported/creatures/2803.png differ diff --git a/art/exported/creatures/2803.png.import b/art/exported/creatures/2803.png.import new file mode 100644 index 0000000..5096057 --- /dev/null +++ b/art/exported/creatures/2803.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjwm07240ehr2" +path.s3tc="res://.godot/imported/2803.png-c2e31a8adbab3bf9899bc74b3feb4abb.s3tc.ctex" +path.etc2="res://.godot/imported/2803.png-c2e31a8adbab3bf9899bc74b3feb4abb.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2803.png" +dest_files=["res://.godot/imported/2803.png-c2e31a8adbab3bf9899bc74b3feb4abb.s3tc.ctex", "res://.godot/imported/2803.png-c2e31a8adbab3bf9899bc74b3feb4abb.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/2804.png b/art/exported/creatures/2804.png new file mode 100644 index 0000000..90952fd Binary files /dev/null and b/art/exported/creatures/2804.png differ diff --git a/art/exported/creatures/2804.png.import b/art/exported/creatures/2804.png.import new file mode 100644 index 0000000..e6147c2 --- /dev/null +++ b/art/exported/creatures/2804.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dgcrfxo1k4juj" +path.s3tc="res://.godot/imported/2804.png-f86c52a01f04302b057cf526a498e87a.s3tc.ctex" +path.etc2="res://.godot/imported/2804.png-f86c52a01f04302b057cf526a498e87a.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2804.png" +dest_files=["res://.godot/imported/2804.png-f86c52a01f04302b057cf526a498e87a.s3tc.ctex", "res://.godot/imported/2804.png-f86c52a01f04302b057cf526a498e87a.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/2806.png b/art/exported/creatures/2806.png new file mode 100644 index 0000000..acb930e Binary files /dev/null and b/art/exported/creatures/2806.png differ diff --git a/art/exported/creatures/2806.png.import b/art/exported/creatures/2806.png.import new file mode 100644 index 0000000..9d444cd --- /dev/null +++ b/art/exported/creatures/2806.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c5be8db5ppkq4" +path.s3tc="res://.godot/imported/2806.png-ed0484fec72605166ddb723c1a1bc7d0.s3tc.ctex" +path.etc2="res://.godot/imported/2806.png-ed0484fec72605166ddb723c1a1bc7d0.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2806.png" +dest_files=["res://.godot/imported/2806.png-ed0484fec72605166ddb723c1a1bc7d0.s3tc.ctex", "res://.godot/imported/2806.png-ed0484fec72605166ddb723c1a1bc7d0.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/2807.png b/art/exported/creatures/2807.png new file mode 100644 index 0000000..5c2a6da Binary files /dev/null and b/art/exported/creatures/2807.png differ diff --git a/art/exported/creatures/2807.png.import b/art/exported/creatures/2807.png.import new file mode 100644 index 0000000..af51ff5 --- /dev/null +++ b/art/exported/creatures/2807.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdus4qo0ebs3e" +path.s3tc="res://.godot/imported/2807.png-aceaf0f586f975d29e318a9513fc3125.s3tc.ctex" +path.etc2="res://.godot/imported/2807.png-aceaf0f586f975d29e318a9513fc3125.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2807.png" +dest_files=["res://.godot/imported/2807.png-aceaf0f586f975d29e318a9513fc3125.s3tc.ctex", "res://.godot/imported/2807.png-aceaf0f586f975d29e318a9513fc3125.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/3002.png b/art/exported/creatures/3002.png new file mode 100644 index 0000000..2ae7e73 Binary files /dev/null and b/art/exported/creatures/3002.png differ diff --git a/art/exported/creatures/3002.png.import b/art/exported/creatures/3002.png.import new file mode 100644 index 0000000..e193cfd --- /dev/null +++ b/art/exported/creatures/3002.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b68vlgtclvkfg" +path.s3tc="res://.godot/imported/3002.png-a87d71158798b43b6bf702a6e1664b4d.s3tc.ctex" +path.etc2="res://.godot/imported/3002.png-a87d71158798b43b6bf702a6e1664b4d.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3002.png" +dest_files=["res://.godot/imported/3002.png-a87d71158798b43b6bf702a6e1664b4d.s3tc.ctex", "res://.godot/imported/3002.png-a87d71158798b43b6bf702a6e1664b4d.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/3102.png b/art/exported/creatures/3102.png new file mode 100644 index 0000000..fe4d8fb Binary files /dev/null and b/art/exported/creatures/3102.png differ diff --git a/art/exported/creatures/3102.png.import b/art/exported/creatures/3102.png.import new file mode 100644 index 0000000..47e9c80 --- /dev/null +++ b/art/exported/creatures/3102.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c4d3f28rf86bb" +path.s3tc="res://.godot/imported/3102.png-951ca90331737e3057749e1bdbd88c44.s3tc.ctex" +path.etc2="res://.godot/imported/3102.png-951ca90331737e3057749e1bdbd88c44.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3102.png" +dest_files=["res://.godot/imported/3102.png-951ca90331737e3057749e1bdbd88c44.s3tc.ctex", "res://.godot/imported/3102.png-951ca90331737e3057749e1bdbd88c44.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/3103.png b/art/exported/creatures/3103.png new file mode 100644 index 0000000..3d3d615 Binary files /dev/null and b/art/exported/creatures/3103.png differ diff --git a/art/exported/creatures/3103.png.import b/art/exported/creatures/3103.png.import new file mode 100644 index 0000000..9743e89 --- /dev/null +++ b/art/exported/creatures/3103.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjtncgnb0hcsj" +path.s3tc="res://.godot/imported/3103.png-f3b4b5bb43b7611f7c448dbdfff36375.s3tc.ctex" +path.etc2="res://.godot/imported/3103.png-f3b4b5bb43b7611f7c448dbdfff36375.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3103.png" +dest_files=["res://.godot/imported/3103.png-f3b4b5bb43b7611f7c448dbdfff36375.s3tc.ctex", "res://.godot/imported/3103.png-f3b4b5bb43b7611f7c448dbdfff36375.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/3104.png b/art/exported/creatures/3104.png new file mode 100644 index 0000000..425932e Binary files /dev/null and b/art/exported/creatures/3104.png differ diff --git a/art/exported/creatures/3104.png.import b/art/exported/creatures/3104.png.import new file mode 100644 index 0000000..fbc70d7 --- /dev/null +++ b/art/exported/creatures/3104.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dlme86lrrhyg8" +path.s3tc="res://.godot/imported/3104.png-fa31dd69bf22b484cf3ac0d99197b886.s3tc.ctex" +path.etc2="res://.godot/imported/3104.png-fa31dd69bf22b484cf3ac0d99197b886.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3104.png" +dest_files=["res://.godot/imported/3104.png-fa31dd69bf22b484cf3ac0d99197b886.s3tc.ctex", "res://.godot/imported/3104.png-fa31dd69bf22b484cf3ac0d99197b886.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/3107.png b/art/exported/creatures/3107.png new file mode 100644 index 0000000..7dee611 Binary files /dev/null and b/art/exported/creatures/3107.png differ diff --git a/art/exported/creatures/3107.png.import b/art/exported/creatures/3107.png.import new file mode 100644 index 0000000..241bac9 --- /dev/null +++ b/art/exported/creatures/3107.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://csv1dsgvifh4l" +path.s3tc="res://.godot/imported/3107.png-5b981d56f7094a29a92b2f5035a47b11.s3tc.ctex" +path.etc2="res://.godot/imported/3107.png-5b981d56f7094a29a92b2f5035a47b11.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3107.png" +dest_files=["res://.godot/imported/3107.png-5b981d56f7094a29a92b2f5035a47b11.s3tc.ctex", "res://.godot/imported/3107.png-5b981d56f7094a29a92b2f5035a47b11.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/3201.png b/art/exported/creatures/3201.png new file mode 100644 index 0000000..bcc639b Binary files /dev/null and b/art/exported/creatures/3201.png differ diff --git a/art/exported/creatures/3201.png.import b/art/exported/creatures/3201.png.import new file mode 100644 index 0000000..a8d6a43 --- /dev/null +++ b/art/exported/creatures/3201.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cd8u0ow8cqsbh" +path.s3tc="res://.godot/imported/3201.png-846cb0fbb88950bb70d5b03c50a88457.s3tc.ctex" +path.etc2="res://.godot/imported/3201.png-846cb0fbb88950bb70d5b03c50a88457.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3201.png" +dest_files=["res://.godot/imported/3201.png-846cb0fbb88950bb70d5b03c50a88457.s3tc.ctex", "res://.godot/imported/3201.png-846cb0fbb88950bb70d5b03c50a88457.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/3401.png b/art/exported/creatures/3401.png new file mode 100644 index 0000000..c14c2ae Binary files /dev/null and b/art/exported/creatures/3401.png differ diff --git a/art/exported/creatures/3401.png.import b/art/exported/creatures/3401.png.import new file mode 100644 index 0000000..096d0fb --- /dev/null +++ b/art/exported/creatures/3401.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://begsj5tj035e1" +path.s3tc="res://.godot/imported/3401.png-cd8c7ec154f8248110ec396b3f8a76c9.s3tc.ctex" +path.etc2="res://.godot/imported/3401.png-cd8c7ec154f8248110ec396b3f8a76c9.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3401.png" +dest_files=["res://.godot/imported/3401.png-cd8c7ec154f8248110ec396b3f8a76c9.s3tc.ctex", "res://.godot/imported/3401.png-cd8c7ec154f8248110ec396b3f8a76c9.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/3902.png b/art/exported/creatures/3902.png new file mode 100644 index 0000000..d231239 Binary files /dev/null and b/art/exported/creatures/3902.png differ diff --git a/art/exported/creatures/3902.png.import b/art/exported/creatures/3902.png.import new file mode 100644 index 0000000..a8e5440 --- /dev/null +++ b/art/exported/creatures/3902.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddc556gcby50v" +path.s3tc="res://.godot/imported/3902.png-485bc4f9f1a505225bdc037111e32499.s3tc.ctex" +path.etc2="res://.godot/imported/3902.png-485bc4f9f1a505225bdc037111e32499.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3902.png" +dest_files=["res://.godot/imported/3902.png-485bc4f9f1a505225bdc037111e32499.s3tc.ctex", "res://.godot/imported/3902.png-485bc4f9f1a505225bdc037111e32499.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/4101.png b/art/exported/creatures/4101.png new file mode 100644 index 0000000..74a068b Binary files /dev/null and b/art/exported/creatures/4101.png differ diff --git a/art/exported/creatures/4101.png.import b/art/exported/creatures/4101.png.import new file mode 100644 index 0000000..d906da5 --- /dev/null +++ b/art/exported/creatures/4101.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bahd6nks2g7ik" +path.s3tc="res://.godot/imported/4101.png-163a8880f3559279d2acc129509430bc.s3tc.ctex" +path.etc2="res://.godot/imported/4101.png-163a8880f3559279d2acc129509430bc.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/4101.png" +dest_files=["res://.godot/imported/4101.png-163a8880f3559279d2acc129509430bc.s3tc.ctex", "res://.godot/imported/4101.png-163a8880f3559279d2acc129509430bc.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/5001.png b/art/exported/creatures/5001.png new file mode 100644 index 0000000..cf39422 Binary files /dev/null and b/art/exported/creatures/5001.png differ diff --git a/art/exported/creatures/5001.png.import b/art/exported/creatures/5001.png.import new file mode 100644 index 0000000..c87c852 --- /dev/null +++ b/art/exported/creatures/5001.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://byil5mr6w3j1q" +path.s3tc="res://.godot/imported/5001.png-ebe37fb8bd4263bb83e36bb5890b5641.s3tc.ctex" +path.etc2="res://.godot/imported/5001.png-ebe37fb8bd4263bb83e36bb5890b5641.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/5001.png" +dest_files=["res://.godot/imported/5001.png-ebe37fb8bd4263bb83e36bb5890b5641.s3tc.ctex", "res://.godot/imported/5001.png-ebe37fb8bd4263bb83e36bb5890b5641.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/5902.png b/art/exported/creatures/5902.png new file mode 100644 index 0000000..809057d Binary files /dev/null and b/art/exported/creatures/5902.png differ diff --git a/art/exported/creatures/5902.png.import b/art/exported/creatures/5902.png.import new file mode 100644 index 0000000..2c8a5fb --- /dev/null +++ b/art/exported/creatures/5902.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://0k4qnt7wusam" +path.s3tc="res://.godot/imported/5902.png-e3a13a1e827488fa86f34c2aad883127.s3tc.ctex" +path.etc2="res://.godot/imported/5902.png-e3a13a1e827488fa86f34c2aad883127.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/5902.png" +dest_files=["res://.godot/imported/5902.png-e3a13a1e827488fa86f34c2aad883127.s3tc.ctex", "res://.godot/imported/5902.png-e3a13a1e827488fa86f34c2aad883127.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/5903.png b/art/exported/creatures/5903.png new file mode 100644 index 0000000..74a8af6 Binary files /dev/null and b/art/exported/creatures/5903.png differ diff --git a/art/exported/creatures/5903.png.import b/art/exported/creatures/5903.png.import new file mode 100644 index 0000000..22946e8 --- /dev/null +++ b/art/exported/creatures/5903.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjb8gqbta2oxw" +path.s3tc="res://.godot/imported/5903.png-44743c2e322f48eefaeedcd3defec27d.s3tc.ctex" +path.etc2="res://.godot/imported/5903.png-44743c2e322f48eefaeedcd3defec27d.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/5903.png" +dest_files=["res://.godot/imported/5903.png-44743c2e322f48eefaeedcd3defec27d.s3tc.ctex", "res://.godot/imported/5903.png-44743c2e322f48eefaeedcd3defec27d.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/5904.png b/art/exported/creatures/5904.png new file mode 100644 index 0000000..924ba4b Binary files /dev/null and b/art/exported/creatures/5904.png differ diff --git a/art/exported/creatures/5904.png.import b/art/exported/creatures/5904.png.import new file mode 100644 index 0000000..23458ae --- /dev/null +++ b/art/exported/creatures/5904.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dtapwjgdbcgr7" +path.s3tc="res://.godot/imported/5904.png-e43ba3245eadac7081a6a23e64ccc940.s3tc.ctex" +path.etc2="res://.godot/imported/5904.png-e43ba3245eadac7081a6a23e64ccc940.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/5904.png" +dest_files=["res://.godot/imported/5904.png-e43ba3245eadac7081a6a23e64ccc940.s3tc.ctex", "res://.godot/imported/5904.png-e43ba3245eadac7081a6a23e64ccc940.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/6001.png b/art/exported/creatures/6001.png new file mode 100644 index 0000000..9219597 Binary files /dev/null and b/art/exported/creatures/6001.png differ diff --git a/art/exported/creatures/6001.png.import b/art/exported/creatures/6001.png.import new file mode 100644 index 0000000..a63cf85 --- /dev/null +++ b/art/exported/creatures/6001.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c0164i40101lq" +path.s3tc="res://.godot/imported/6001.png-6fd8c3ef0885c466baece687cd24d38d.s3tc.ctex" +path.etc2="res://.godot/imported/6001.png-6fd8c3ef0885c466baece687cd24d38d.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6001.png" +dest_files=["res://.godot/imported/6001.png-6fd8c3ef0885c466baece687cd24d38d.s3tc.ctex", "res://.godot/imported/6001.png-6fd8c3ef0885c466baece687cd24d38d.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/6406.png b/art/exported/creatures/6406.png new file mode 100644 index 0000000..15c3b83 Binary files /dev/null and b/art/exported/creatures/6406.png differ diff --git a/art/exported/creatures/6406.png.import b/art/exported/creatures/6406.png.import new file mode 100644 index 0000000..ac060ee --- /dev/null +++ b/art/exported/creatures/6406.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://gnlehenln3kp" +path.s3tc="res://.godot/imported/6406.png-67ebebdd8d13cd88e0a209b3ed00024c.s3tc.ctex" +path.etc2="res://.godot/imported/6406.png-67ebebdd8d13cd88e0a209b3ed00024c.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6406.png" +dest_files=["res://.godot/imported/6406.png-67ebebdd8d13cd88e0a209b3ed00024c.s3tc.ctex", "res://.godot/imported/6406.png-67ebebdd8d13cd88e0a209b3ed00024c.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/6601.png b/art/exported/creatures/6601.png new file mode 100644 index 0000000..93c05c0 Binary files /dev/null and b/art/exported/creatures/6601.png differ diff --git a/art/exported/creatures/6601.png.import b/art/exported/creatures/6601.png.import new file mode 100644 index 0000000..f5facf7 --- /dev/null +++ b/art/exported/creatures/6601.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://jauudro8rgay" +path.s3tc="res://.godot/imported/6601.png-7d5f9459e449f3aa7ae72a9e069e6a21.s3tc.ctex" +path.etc2="res://.godot/imported/6601.png-7d5f9459e449f3aa7ae72a9e069e6a21.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6601.png" +dest_files=["res://.godot/imported/6601.png-7d5f9459e449f3aa7ae72a9e069e6a21.s3tc.ctex", "res://.godot/imported/6601.png-7d5f9459e449f3aa7ae72a9e069e6a21.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/6801.png b/art/exported/creatures/6801.png new file mode 100644 index 0000000..bcdf70b Binary files /dev/null and b/art/exported/creatures/6801.png differ diff --git a/art/exported/creatures/6801.png.import b/art/exported/creatures/6801.png.import new file mode 100644 index 0000000..c257a50 --- /dev/null +++ b/art/exported/creatures/6801.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cuik8y4tqqetk" +path.s3tc="res://.godot/imported/6801.png-f771f612f1db1d1b99cb446dfebc4992.s3tc.ctex" +path.etc2="res://.godot/imported/6801.png-f771f612f1db1d1b99cb446dfebc4992.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6801.png" +dest_files=["res://.godot/imported/6801.png-f771f612f1db1d1b99cb446dfebc4992.s3tc.ctex", "res://.godot/imported/6801.png-f771f612f1db1d1b99cb446dfebc4992.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/6807.png b/art/exported/creatures/6807.png new file mode 100644 index 0000000..270004f Binary files /dev/null and b/art/exported/creatures/6807.png differ diff --git a/art/exported/creatures/6807.png.import b/art/exported/creatures/6807.png.import new file mode 100644 index 0000000..026d3d9 --- /dev/null +++ b/art/exported/creatures/6807.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cu5s2rxrc0dh5" +path.s3tc="res://.godot/imported/6807.png-35d499aa8b3f10cc974cc7df8b70f89e.s3tc.ctex" +path.etc2="res://.godot/imported/6807.png-35d499aa8b3f10cc974cc7df8b70f89e.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6807.png" +dest_files=["res://.godot/imported/6807.png-35d499aa8b3f10cc974cc7df8b70f89e.s3tc.ctex", "res://.godot/imported/6807.png-35d499aa8b3f10cc974cc7df8b70f89e.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/6901.png b/art/exported/creatures/6901.png new file mode 100644 index 0000000..0c34e37 Binary files /dev/null and b/art/exported/creatures/6901.png differ diff --git a/art/exported/creatures/6901.png.import b/art/exported/creatures/6901.png.import new file mode 100644 index 0000000..651f116 --- /dev/null +++ b/art/exported/creatures/6901.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://umjy47x208xb" +path.s3tc="res://.godot/imported/6901.png-f9ff2d1fc8e00eabd73822b5031f91d8.s3tc.ctex" +path.etc2="res://.godot/imported/6901.png-f9ff2d1fc8e00eabd73822b5031f91d8.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6901.png" +dest_files=["res://.godot/imported/6901.png-f9ff2d1fc8e00eabd73822b5031f91d8.s3tc.ctex", "res://.godot/imported/6901.png-f9ff2d1fc8e00eabd73822b5031f91d8.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/6902.png b/art/exported/creatures/6902.png new file mode 100644 index 0000000..3d4517e Binary files /dev/null and b/art/exported/creatures/6902.png differ diff --git a/art/exported/creatures/6902.png.import b/art/exported/creatures/6902.png.import new file mode 100644 index 0000000..35f84df --- /dev/null +++ b/art/exported/creatures/6902.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://csdl6u3uvft45" +path.s3tc="res://.godot/imported/6902.png-f8899611759244b42c6c786ff118dcba.s3tc.ctex" +path.etc2="res://.godot/imported/6902.png-f8899611759244b42c6c786ff118dcba.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6902.png" +dest_files=["res://.godot/imported/6902.png-f8899611759244b42c6c786ff118dcba.s3tc.ctex", "res://.godot/imported/6902.png-f8899611759244b42c6c786ff118dcba.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/6904.png b/art/exported/creatures/6904.png new file mode 100644 index 0000000..f2e8a7b Binary files /dev/null and b/art/exported/creatures/6904.png differ diff --git a/art/exported/creatures/6904.png.import b/art/exported/creatures/6904.png.import new file mode 100644 index 0000000..75051e3 --- /dev/null +++ b/art/exported/creatures/6904.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqh7d4v8jrbpr" +path.s3tc="res://.godot/imported/6904.png-38739e4e48fbb83e3f65074a93d681bb.s3tc.ctex" +path.etc2="res://.godot/imported/6904.png-38739e4e48fbb83e3f65074a93d681bb.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6904.png" +dest_files=["res://.godot/imported/6904.png-38739e4e48fbb83e3f65074a93d681bb.s3tc.ctex", "res://.godot/imported/6904.png-38739e4e48fbb83e3f65074a93d681bb.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.glb b/art/exported/items/equipment/net.glb new file mode 100644 index 0000000..db4fa34 Binary files /dev/null and b/art/exported/items/equipment/net.glb differ diff --git a/art/exported/items/equipment/net.glb.import b/art/exported/items/equipment/net.glb.import new file mode 100644 index 0000000..28667d6 --- /dev/null +++ b/art/exported/items/equipment/net.glb.import @@ -0,0 +1,45 @@ +[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 new file mode 100644 index 0000000..bd5eff2 Binary files /dev/null and b/art/exported/items/equipment/net_Material.png differ diff --git a/art/exported/items/equipment/net_Material.png.import b/art/exported/items/equipment/net_Material.png.import new file mode 100644 index 0000000..0723973 --- /dev/null +++ b/art/exported/items/equipment/net_Material.png.import @@ -0,0 +1,45 @@ +[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 new file mode 100644 index 0000000..795d0a3 Binary files /dev/null and b/art/exported/items/equipment/net_Plane Base Color.png 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 new file mode 100644 index 0000000..61077be --- /dev/null +++ b/art/exported/items/equipment/net_Plane Base Color.png.import @@ -0,0 +1,45 @@ +[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/netfishing.ico b/art/exported/system_icons/netfishing.ico deleted file mode 100644 index dc5bdae..0000000 Binary files a/art/exported/system_icons/netfishing.ico and /dev/null differ diff --git a/art/exported/system_icons/netfishing_1024.png b/art/exported/system_icons/netfishing_1024.png deleted file mode 100644 index a433e80..0000000 Binary files a/art/exported/system_icons/netfishing_1024.png and /dev/null differ diff --git a/art/exported/system_icons/netfishing_256.png b/art/exported/system_icons/netfishing_256.png deleted file mode 100644 index 1e7be72..0000000 Binary files a/art/exported/system_icons/netfishing_256.png and /dev/null differ diff --git a/art/exported/system_icons/straywild.ico b/art/exported/system_icons/straywild.ico new file mode 100644 index 0000000..c03de8e Binary files /dev/null and b/art/exported/system_icons/straywild.ico differ diff --git a/art/exported/system_icons/straywild_1024.png b/art/exported/system_icons/straywild_1024.png new file mode 100644 index 0000000..2a3b204 Binary files /dev/null and b/art/exported/system_icons/straywild_1024.png differ diff --git a/art/exported/system_icons/netfishing_1024.png.import b/art/exported/system_icons/straywild_1024.png.import similarity index 74% rename from art/exported/system_icons/netfishing_1024.png.import rename to art/exported/system_icons/straywild_1024.png.import index 08aa925..dea156a 100644 --- a/art/exported/system_icons/netfishing_1024.png.import +++ b/art/exported/system_icons/straywild_1024.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://bkh5hw8wwu8pc" -path="res://.godot/imported/netfishing_1024.png-7c53a099ed2743b4a623c258641b1d29.ctex" +path="res://.godot/imported/straywild_1024.png-b9b5c159c9fcecc21b04324370d4522f.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://art/exported/system_icons/netfishing_1024.png" -dest_files=["res://.godot/imported/netfishing_1024.png-7c53a099ed2743b4a623c258641b1d29.ctex"] +source_file="res://art/exported/system_icons/straywild_1024.png" +dest_files=["res://.godot/imported/straywild_1024.png-b9b5c159c9fcecc21b04324370d4522f.ctex"] [params] diff --git a/art/exported/system_icons/straywild_256.png b/art/exported/system_icons/straywild_256.png new file mode 100644 index 0000000..98275eb Binary files /dev/null and b/art/exported/system_icons/straywild_256.png differ diff --git a/art/exported/system_icons/netfishing_256.png.import b/art/exported/system_icons/straywild_256.png.import similarity index 74% rename from art/exported/system_icons/netfishing_256.png.import rename to art/exported/system_icons/straywild_256.png.import index 34d5d27..5726347 100644 --- a/art/exported/system_icons/netfishing_256.png.import +++ b/art/exported/system_icons/straywild_256.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://cty16m8ydla06" -path="res://.godot/imported/netfishing_256.png-d9dfc9c7871ac38bc73d09d00df679be.ctex" +path="res://.godot/imported/straywild_256.png-87b7e62e710eae4d3e3ed24a8454ca6d.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://art/exported/system_icons/netfishing_256.png" -dest_files=["res://.godot/imported/netfishing_256.png-d9dfc9c7871ac38bc73d09d00df679be.ctex"] +source_file="res://art/exported/system_icons/straywild_256.png" +dest_files=["res://.godot/imported/straywild_256.png-87b7e62e710eae4d3e3ed24a8454ca6d.ctex"] [params] diff --git a/art/source/characters/base/netfishing_base_character.blend b/art/source/characters/base/straywild_base_character.blend similarity index 100% rename from art/source/characters/base/netfishing_base_character.blend rename to art/source/characters/base/straywild_base_character.blend diff --git a/art/source/characters/base/netfishing_base_character.blend.import b/art/source/characters/base/straywild_base_character.blend.import similarity index 84% rename from art/source/characters/base/netfishing_base_character.blend.import rename to art/source/characters/base/straywild_base_character.blend.import index f6df489..2cab53c 100644 --- a/art/source/characters/base/netfishing_base_character.blend.import +++ b/art/source/characters/base/straywild_base_character.blend.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://cb4bcakhoprm8" -path="res://.godot/imported/netfishing_base_character.blend-65bf3bc5b77cc1a37fa3f81ebd808d37.scn" +path="res://.godot/imported/straywild_base_character.blend-02abbfdb9b1d393f1bd3cfface615e27.scn" [deps] -source_file="res://art/source/characters/base/netfishing_base_character.blend" -dest_files=["res://.godot/imported/netfishing_base_character.blend-65bf3bc5b77cc1a37fa3f81ebd808d37.scn"] +source_file="res://art/source/characters/base/straywild_base_character.blend" +dest_files=["res://.godot/imported/straywild_base_character.blend-02abbfdb9b1d393f1bd3cfface615e27.scn"] [params] diff --git a/art/source/characters/base/netfishing_base_character.glb.import b/art/source/characters/base/straywild_base_character.glb.import similarity index 77% rename from art/source/characters/base/netfishing_base_character.glb.import rename to art/source/characters/base/straywild_base_character.glb.import index 5a38b76..bda93c0 100644 --- a/art/source/characters/base/netfishing_base_character.glb.import +++ b/art/source/characters/base/straywild_base_character.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://bfhnup14638r7" -path="res://.godot/imported/netfishing_base_character.glb-cbdc33eaa4fcd1d2d2a8f4a463b228d6.scn" +path="res://.godot/imported/straywild_base_character.glb-c9650c715a1b5b9ae0d8541191f7d7a9.scn" [deps] -source_file="res://art/source/characters/base/netfishing_base_character.glb" -dest_files=["res://.godot/imported/netfishing_base_character.glb-cbdc33eaa4fcd1d2d2a8f4a463b228d6.scn"] +source_file="res://art/source/characters/base/straywild_base_character.glb" +dest_files=["res://.godot/imported/straywild_base_character.glb-c9650c715a1b5b9ae0d8541191f7d7a9.scn"] [params] diff --git a/art/source/characters/base/netfishing_character_source.blend b/art/source/characters/base/straywild_character_source.blend similarity index 100% rename from art/source/characters/base/netfishing_character_source.blend rename to art/source/characters/base/straywild_character_source.blend diff --git a/art/source/characters/base/netfishing_character_source.blend.import b/art/source/characters/base/straywild_character_source.blend.import similarity index 84% rename from art/source/characters/base/netfishing_character_source.blend.import rename to art/source/characters/base/straywild_character_source.blend.import index 8288280..741c389 100644 --- a/art/source/characters/base/netfishing_character_source.blend.import +++ b/art/source/characters/base/straywild_character_source.blend.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://d38y8blk2w5c5" -path="res://.godot/imported/netfishing_character_source.blend-1d18c95606937d4c8bcb87e334e02ee5.scn" +path="res://.godot/imported/straywild_character_source.blend-0e7b8a87008f88e309ad73a63561ef5b.scn" [deps] -source_file="res://art/source/characters/base/netfishing_character_source.blend" -dest_files=["res://.godot/imported/netfishing_character_source.blend-1d18c95606937d4c8bcb87e334e02ee5.scn"] +source_file="res://art/source/characters/base/straywild_character_source.blend" +dest_files=["res://.godot/imported/straywild_character_source.blend-0e7b8a87008f88e309ad73a63561ef5b.scn"] [params] diff --git a/art/source/characters/base/netfishing_character_source.glb.import b/art/source/characters/base/straywild_character_source.glb.import similarity index 76% rename from art/source/characters/base/netfishing_character_source.glb.import rename to art/source/characters/base/straywild_character_source.glb.import index 61158c5..bbcee6a 100644 --- a/art/source/characters/base/netfishing_character_source.glb.import +++ b/art/source/characters/base/straywild_character_source.glb.import @@ -4,12 +4,12 @@ importer="scene" importer_version=1 type="PackedScene" uid="uid://3uq4cxiomttt" -path="res://.godot/imported/netfishing_character_source.glb-9c98ee44b142c5b3d06f0838b7d2c9c4.scn" +path="res://.godot/imported/straywild_character_source.glb-3a8ccc55dbe60da1b70b62110b206353.scn" [deps] -source_file="res://art/source/characters/base/netfishing_character_source.glb" -dest_files=["res://.godot/imported/netfishing_character_source.glb-9c98ee44b142c5b3d06f0838b7d2c9c4.scn"] +source_file="res://art/source/characters/base/straywild_character_source.glb" +dest_files=["res://.godot/imported/straywild_character_source.glb-3a8ccc55dbe60da1b70b62110b206353.scn"] [params] diff --git a/art/source/characters/base/netfishing_character_source_eyes1_eyebrows1.png b/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png similarity index 100% rename from art/source/characters/base/netfishing_character_source_eyes1_eyebrows1.png rename to art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png diff --git a/art/source/characters/base/netfishing_character_source_eyes1_eyebrows1.png.import b/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png.import similarity index 58% rename from art/source/characters/base/netfishing_character_source_eyes1_eyebrows1.png.import rename to art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png.import index e7aade1..dba5a5f 100644 --- a/art/source/characters/base/netfishing_character_source_eyes1_eyebrows1.png.import +++ b/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://bdomsntfeovnc" -path.s3tc="res://.godot/imported/netfishing_character_source_eyes1_eyebrows1.png-a0b9f287e407afd9688e141dc5f2b801.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_character_source_eyes1_eyebrows1.png-a0b9f287e407afd9688e141dc5f2b801.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/source/characters/base/netfishing_character_source_eyes1_eyebrows1.png" -dest_files=["res://.godot/imported/netfishing_character_source_eyes1_eyebrows1.png-a0b9f287e407afd9688e141dc5f2b801.s3tc.ctex", "res://.godot/imported/netfishing_character_source_eyes1_eyebrows1.png-a0b9f287e407afd9688e141dc5f2b801.etc2.ctex"] +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] diff --git a/art/source/characters/base/netfishing_character_source_eyes_simple_shine.png b/art/source/characters/base/straywild_character_source_eyes_simple_shine.png similarity index 100% rename from art/source/characters/base/netfishing_character_source_eyes_simple_shine.png rename to art/source/characters/base/straywild_character_source_eyes_simple_shine.png diff --git a/art/source/characters/base/netfishing_character_source_eyes_simple_shine.png.import b/art/source/characters/base/straywild_character_source_eyes_simple_shine.png.import similarity index 58% rename from art/source/characters/base/netfishing_character_source_eyes_simple_shine.png.import rename to art/source/characters/base/straywild_character_source_eyes_simple_shine.png.import index e49d401..8b99f72 100644 --- a/art/source/characters/base/netfishing_character_source_eyes_simple_shine.png.import +++ b/art/source/characters/base/straywild_character_source_eyes_simple_shine.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://du4u6meapay05" -path.s3tc="res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/source/characters/base/netfishing_character_source_eyes_simple_shine.png" -dest_files=["res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.s3tc.ctex", "res://.godot/imported/netfishing_character_source_eyes_simple_shine.png-965743f3716320bbc385096956ffef2d.etc2.ctex"] +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] diff --git a/art/source/characters/base/netfishing_character_source_mouth1.png b/art/source/characters/base/straywild_character_source_mouth1.png similarity index 100% rename from art/source/characters/base/netfishing_character_source_mouth1.png rename to art/source/characters/base/straywild_character_source_mouth1.png diff --git a/art/source/characters/base/netfishing_character_source_mouth1.png.import b/art/source/characters/base/straywild_character_source_mouth1.png.import similarity index 60% rename from art/source/characters/base/netfishing_character_source_mouth1.png.import rename to art/source/characters/base/straywild_character_source_mouth1.png.import index 8ff49a4..5dd84f4 100644 --- a/art/source/characters/base/netfishing_character_source_mouth1.png.import +++ b/art/source/characters/base/straywild_character_source_mouth1.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://css0k0virgoph" -path.s3tc="res://.godot/imported/netfishing_character_source_mouth1.png-b323b9153acd3f552f35c1d1108567c8.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_character_source_mouth1.png-b323b9153acd3f552f35c1d1108567c8.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/source/characters/base/netfishing_character_source_mouth1.png" -dest_files=["res://.godot/imported/netfishing_character_source_mouth1.png-b323b9153acd3f552f35c1d1108567c8.s3tc.ctex", "res://.godot/imported/netfishing_character_source_mouth1.png-b323b9153acd3f552f35c1d1108567c8.etc2.ctex"] +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] diff --git a/art/source/characters/base/netfishing_character_source_mouth_three.png b/art/source/characters/base/straywild_character_source_mouth_three.png similarity index 100% rename from art/source/characters/base/netfishing_character_source_mouth_three.png rename to art/source/characters/base/straywild_character_source_mouth_three.png diff --git a/art/source/characters/base/netfishing_character_source_mouth_three.png.import b/art/source/characters/base/straywild_character_source_mouth_three.png.import similarity index 59% rename from art/source/characters/base/netfishing_character_source_mouth_three.png.import rename to art/source/characters/base/straywild_character_source_mouth_three.png.import index 6abae09..4b2413d 100644 --- a/art/source/characters/base/netfishing_character_source_mouth_three.png.import +++ b/art/source/characters/base/straywild_character_source_mouth_three.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://di20c1x8vksi1" -path.s3tc="res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/source/characters/base/netfishing_character_source_mouth_three.png" -dest_files=["res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.s3tc.ctex", "res://.godot/imported/netfishing_character_source_mouth_three.png-6bf79d4c9c660d30fb7fc365f0039313.etc2.ctex"] +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] diff --git a/art/source/characters/base/netfishing_character_source_nose1.2.png b/art/source/characters/base/straywild_character_source_nose1.2.png similarity index 100% rename from art/source/characters/base/netfishing_character_source_nose1.2.png rename to art/source/characters/base/straywild_character_source_nose1.2.png diff --git a/art/source/characters/base/netfishing_character_source_nose1.2.png.import b/art/source/characters/base/straywild_character_source_nose1.2.png.import similarity index 60% rename from art/source/characters/base/netfishing_character_source_nose1.2.png.import rename to art/source/characters/base/straywild_character_source_nose1.2.png.import index 2fbb165..e0c3667 100644 --- a/art/source/characters/base/netfishing_character_source_nose1.2.png.import +++ b/art/source/characters/base/straywild_character_source_nose1.2.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://7dwa0bfev1rk" -path.s3tc="res://.godot/imported/netfishing_character_source_nose1.2.png-ef4dd456c5b0f0db73b3dc063bfba941.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_character_source_nose1.2.png-ef4dd456c5b0f0db73b3dc063bfba941.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/source/characters/base/netfishing_character_source_nose1.2.png" -dest_files=["res://.godot/imported/netfishing_character_source_nose1.2.png-ef4dd456c5b0f0db73b3dc063bfba941.s3tc.ctex", "res://.godot/imported/netfishing_character_source_nose1.2.png-ef4dd456c5b0f0db73b3dc063bfba941.etc2.ctex"] +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] diff --git a/art/source/characters/base/netfishing_character_source_nose_dog_round.png b/art/source/characters/base/straywild_character_source_nose_dog_round.png similarity index 100% rename from art/source/characters/base/netfishing_character_source_nose_dog_round.png rename to art/source/characters/base/straywild_character_source_nose_dog_round.png diff --git a/art/source/characters/base/netfishing_character_source_nose_dog_round.png.import b/art/source/characters/base/straywild_character_source_nose_dog_round.png.import similarity index 59% rename from art/source/characters/base/netfishing_character_source_nose_dog_round.png.import rename to art/source/characters/base/straywild_character_source_nose_dog_round.png.import index 12333e8..5fe079e 100644 --- a/art/source/characters/base/netfishing_character_source_nose_dog_round.png.import +++ b/art/source/characters/base/straywild_character_source_nose_dog_round.png.import @@ -3,8 +3,8 @@ importer="texture" type="CompressedTexture2D" uid="uid://b0p55hwdry2cr" -path.s3tc="res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.s3tc.ctex" -path.etc2="res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.etc2.ctex" +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 @@ -15,8 +15,8 @@ generator_parameters={ [deps] -source_file="res://art/source/characters/base/netfishing_character_source_nose_dog_round.png" -dest_files=["res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.s3tc.ctex", "res://.godot/imported/netfishing_character_source_nose_dog_round.png-1c89150861ebff3f930698c1747a0369.etc2.ctex"] +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] diff --git a/art/source/items/equipment/net.blend b/art/source/items/equipment/net.blend new file mode 100644 index 0000000..0a7ba4c Binary files /dev/null and b/art/source/items/equipment/net.blend differ diff --git a/docs/ATTRIBUTION.md b/docs/ATTRIBUTION.md index d24f1ca..d8676f0 100644 --- a/docs/ATTRIBUTION.md +++ b/docs/ATTRIBUTION.md @@ -7,7 +7,7 @@ prove authorship or licensing. ## Ownership and credits -NETfishing is jointly owned, developed, and published by its creators, +straywild is jointly owned, developed, and published by its creators, publicly credited as Voyager and Endeavour and operating collectively as the independent team Woofmeow. @@ -31,7 +31,7 @@ independent team Woofmeow. 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 NETfishing, Woofmeow, or its owners. +credits do not imply endorsement of straywild, Woofmeow, or its owners. ## License map @@ -41,7 +41,7 @@ Do not describe the complete repository as covered only by the GPL. | --- | --- | | 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` | -| NETfishing and Woofmeow names, logos, icons, and official identity | Source-root or packaged `TRADEMARKS.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 | @@ -50,22 +50,22 @@ 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 NETfishing creative assets or branding. The -NETfishing Asset License separately permits qualifying noncommercial add-on -Mods for official NETfishing software and narrowly scoped Contribution Forks +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 NETfishing assets and reserved +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 NETfishing owners under their private joint +written approval from both straywild owners under their private joint ownership agreement. ## Godot Engine notice -NETfishing uses Godot Engine under the MIT License: +straywild uses Godot Engine under the MIT License: > Copyright (c) 2014-present Godot Engine contributors. > Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. @@ -103,7 +103,7 @@ FreeType for font rendering; portions are copyright The FreeType Project 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 NETfishing Asset License. +Public-domain fonts are not governed by the straywild Asset License. ## Intake policy @@ -122,7 +122,7 @@ 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. -NETfishing does not accept unsolicited contributions. For work expressly +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. @@ -142,8 +142,10 @@ 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. Specific contributor exceptions are recorded below and in the -in-game credits. +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 @@ -159,8 +161,8 @@ 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 NETfishing community member kat. | -| Kim animalese alphabet and number sounds | `sound/dialogue/animalese/kim/` | Project-processed derivatives of the original source voice recording supplied by NETfishing community member kim. | +| 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. | diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 09db779..76ed032 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,6 +1,6 @@ # Development reference -This is the authoritative reference for NETfishing architecture, engineering +This is the authoritative reference for straywild architecture, engineering decisions, content policy, authoring, and validation. ## Architecture @@ -43,7 +43,7 @@ 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 `NETFISHING_DISCOVERY_URL` available as a +`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, @@ -261,7 +261,7 @@ copies of the entire customization catalog. ### Texture sampling -NETfishing artwork uses nearest-neighbor sampling. Do not enable linear, +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 @@ -351,10 +351,10 @@ godot --headless --path . --script tests/fish_catalog_content_validation.gd rm -rf -- "$test_root" ``` -Do not point `NETFISHING_DATA_DIR` at an arbitrary empty directory; it is an +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 -`NETFISHING_CREATE_DATA_DIR=1`, allowing the game to initialize its known empty +`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, diff --git a/docs/PORTMASTER.md b/docs/PORTMASTER.md index e8c24f6..803f72f 100644 --- a/docs/PORTMASTER.md +++ b/docs/PORTMASTER.md @@ -1,15 +1,15 @@ # PortMaster reference -NETfishing's PortMaster release is a canonical PortMaster package, not a +straywild's PortMaster release is a canonical PortMaster package, not a version-named generic ZIP. The installer-facing archive must be named -`netfishing.zip`, and its root must contain exactly: +`straywild.zip`, and its root must contain exactly: ```text -NETfishing.sh -netfishing/ +straywild.sh +straywild/ ``` -`port.json` belongs at `netfishing/port.json` in the distributable archive. +`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 @@ -17,7 +17,7 @@ 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 NETfishing's former root-level `port.json` during auto-install tests. +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. @@ -26,30 +26,34 @@ directory. The templates in `scripts/portmaster/` are authoritative. ## Release contract -- `port.json` uses schema version 4, names `netfishing.zip`, and marks the +- `port.json` uses schema version 4, names `straywild.zip`, and marks the self-contained package ready to run. -- `NETfishing.sh` does not contain a `# PORTMASTER:` signature in the source or +- `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 `netfishing/NETfishing.aarch64`. +- 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 `netfishing.gptk` no-op map. +- 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 NETfishing's controller + 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. -- NETfishing automatically creates player data for new installations under - `ports/saves/netfishing/` and does not present the in-game folder picker. +- 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 - `netfishing/conf/data`, `netfishing/conf/config`, and - `netfishing/conf/cache`. + `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 `netfishing.zip` for this +- 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. @@ -67,7 +71,7 @@ The script deletes and regenerates the Linux ARM64 export, stages the package, validates its structure, and writes: ```text -builds/v/netfishing.zip +builds/v/straywild.zip ``` `--package-only` is reserved for repackaging an already validated ARM64 export. @@ -83,7 +87,7 @@ 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 `netfishing.zip`; never move the game release tag. +the release's existing `straywild.zip`; never move the game release tag. ## Local muOS installation @@ -93,22 +97,22 @@ The muOS auto-install directory is: /mnt/mmc/MUOS/PortMaster/autoinstall/ ``` -For an explicit offline installation test, copy `netfishing.zip` to a temporary +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 ./netfishing.zip + --offline --no-check install ./straywild.zip ``` -Before upgrading an existing installation, preserve -`/mnt/mmc/ports/netfishing/conf/` and `/mnt/mmc/ports/saves/netfishing/` on the -device. After installation: +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 - `netfishing.gptk` and calls `pm_platform_helper` for the game executable. + `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. @@ -117,7 +121,7 @@ device. After installation: Start+Select on most devices. Installing the public PortMaster catalog entry can still install an older -catalog build until the upstream `netfishing.zip` is updated. A local release +catalog build until the upstream `straywild.zip` is updated. A local release test must install the generated local archive explicitly. ## Template provenance @@ -130,7 +134,7 @@ f24daae48f543b3a270a5aa46b5e73a31904bf0c7216d4c8a9e15a8dc48a5eed screenshot.png ``` The remaining catalog metadata assets were copied from the installed official -`netfishing.zip` package and are intentionally preserved byte-for-byte: +`straywild.zip` package and are intentionally preserved byte-for-byte: ```text e2a9d132744684c67865c01eca027a8c9946b4c5a19da57c82f1af0373dc83b7 gameinfo.xml @@ -160,7 +164,7 @@ The device-verified Godot mapping is: - 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/NETfishing.sh` must replace the incompatible SDL2 entry +`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 @@ -169,14 +173,14 @@ 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-netfishing.sh`, which reads that +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 `netfishing.gptk`, which explicitly unassigns every direct and +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 @@ -209,8 +213,8 @@ The light profile passes these Godot options: - `--max-fps 30` - `--audio-output-latency 40` -It also passes `NETFISHING_PERFORMANCE_PROFILE=light` and -`NETFISHING_LOW_END=1`. The game renders the 3D world at 37.5% linear +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: @@ -244,7 +248,7 @@ 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 -`netfishing/conf/performance_profile` containing exactly: +`straywild/conf/performance_profile` containing exactly: ```text normal @@ -253,13 +257,13 @@ normal On a typical muOS installation, the full path is: ```text -/mnt/mmc/ports/netfishing/conf/performance_profile +/mnt/mmc/ports/straywild/conf/performance_profile ``` Set the file to `light`, or remove it, to restore the default. An externally -provided `NETFISHING_PERFORMANCE_PROFILE=normal` or `light` environment value +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 `netfishing/log.txt`. +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 diff --git a/docs/README-PLAYTEST.txt b/docs/README-PLAYTEST.txt index 0268032..f187182 100644 --- a/docs/README-PLAYTEST.txt +++ b/docs/README-PLAYTEST.txt @@ -1,10 +1,10 @@ -NETfishing -v0.18.0-alpha -Alpha 0.18.0 +straywild +v0.20.2-alpha +Alpha 0.20.2 -Thank you for trying this early private playtest. +Thank you for supporting this private testing release. -NETfishing is an early multiplayer fishing game. This alpha includes local +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. @@ -12,15 +12,15 @@ 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 netfishing.pck together. -3. Launch netfishing.exe on Windows or netfishing on Linux. +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 netfishing + chmod +x straywild CONTROLS @@ -83,6 +83,8 @@ FEATURES TO TRY - 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 @@ -117,5 +119,6 @@ KNOWN ALPHA LIMITATIONS 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 index 05ac0b4..444ebc7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# NETfishing documentation +# straywild documentation This directory is the single home for maintained project documentation. Start here instead of searching feature, asset, world, or script directories @@ -17,10 +17,10 @@ for local README files. ## Related repositories -- [Discovery service API](https://forge.makearmy.io/woofmeow/netfishing-discovery-server/src/branch/main/docs/API.md) +- [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/netfishing-dedicated-server/src/branch/main/README.md) +- [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. diff --git a/export_presets.cfg b/export_presets.cfg index 340cce4..a49f01f 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -9,7 +9,7 @@ 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.18.0-alpha/windows-x86_64/NETfishing.exe" +export_path="builds/v0.20.2-alpha/windows-x86_64/straywild.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/netfishing.ico" +application/icon="res://art/exported/system_icons/straywild.ico" application/console_wrapper_icon="" -application/icon_interpolation=4 -application/file_version="0.18.0.0" -application/product_version="0.18.0.0" +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="NETfishing" -application/file_description="NETfishing v0.18.0-alpha" +application/product_name="straywild" +application/file_description="straywild v0.20.2-alpha" application/copyright="Copyright © 2026 Woofmeow" -application/trademarks="NETfishing and Woofmeow branding is reserved; see TRADEMARKS.md" +application/trademarks="straywild and Woofmeow branding is reserved; see TRADEMARKS.md" application/export_angle=0 ssh_remote_deploy/enabled=false ssh_remote_deploy/host="user@host_ip" @@ -62,7 +62,7 @@ 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.18.0-alpha/linux-arm64/NETfishing.arm64" +export_path="builds/v0.20.2-alpha/linux-arm64/straywild.arm64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -92,7 +92,7 @@ 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.18.0-alpha/macos/NETfishing.zip" +export_path="builds/v0.20.2-alpha/macos/straywild.zip" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -106,10 +106,10 @@ script_export_mode=2 custom_template/debug="" custom_template/release="" application/bundle_identifier="io.woofmeow.netfishing" -application/icon="res://art/exported/system_icons/netfishing_1024.png" +application/icon="res://art/exported/system_icons/straywild_1024.png" application/icon_interpolation=0 -application/short_version="0.18.0" -application/version="0.18.0" +application/short_version="0.20.2" +application/version="0.20.2" application/architecture="universal" codesign/enable=false notarization/enable=false @@ -125,7 +125,7 @@ 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.18.0-alpha/server-linux-x86_64/NETfishingServer.x86_64" +export_path="builds/v0.20.2-alpha/server-linux-x86_64/straywildServer.x86_64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -155,7 +155,7 @@ 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.18.0-alpha/android/NETfishing.apk" +export_path="builds/v0.20.2-alpha/android/straywild.apk" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -176,10 +176,10 @@ architectures/armeabi-v7a=false architectures/arm64-v8a=true architectures/x86=false architectures/x86_64=false -version/code=180000 -version/name="v0.18.0-alpha" +version/code=200200 +version/name="v0.20.2-alpha" package/unique_name="io.woofmeow.netfishing" -package/name="NETfishing" +package/name="straywild" package/signed=true package/app_category=0 package/retain_data_on_uninstall=false @@ -187,7 +187,7 @@ 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/netfishing_256.png" +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 @@ -214,7 +214,7 @@ 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.18.0-alpha/linux-x86_64/NETfishing.x86_64" +export_path="builds/v0.20.2-alpha/linux-x86_64/straywild.x86_64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" diff --git a/fish/species/bluegill/bluegill.tres b/fish/species/bluegill/bluegill.tres index 1ad5656..181b969 100644 --- a/fish/species/bluegill/bluegill.tres +++ b/fish/species/bluegill/bluegill.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/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="Texture2D" path="res://art/exported/creatures/3102.png" id="4_texture"] [resource] script = ExtResource("1_fish_data") diff --git a/fish/species/bream_common/bream_common.tres b/fish/species/bream_common/bream_common.tres index 1c7398c..cc44fef 100644 --- a/fish/species/bream_common/bream_common.tres +++ b/fish/species/bream_common/bream_common.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2502.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/chub_creek/chub_creek.tres b/fish/species/chub_creek/chub_creek.tres index 1739712..778e9da 100644 --- a/fish/species/chub_creek/chub_creek.tres +++ b/fish/species/chub_creek/chub_creek.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2802.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/chub_european/chub_european.tres b/fish/species/chub_european/chub_european.tres index a218aec..16f43a1 100644 --- a/fish/species/chub_european/chub_european.tres +++ b/fish/species/chub_european/chub_european.tres @@ -3,7 +3,7 @@ [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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2505.png" id="4_texture"] [sub_resource type="Resource" id="ChubEuropeanAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/chub_lake/chub_lake.tres b/fish/species/chub_lake/chub_lake.tres index 6da6b43..1dc2d9a 100644 --- a/fish/species/chub_lake/chub_lake.tres +++ b/fish/species/chub_lake/chub_lake.tres @@ -3,7 +3,7 @@ [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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2807.png" id="4_texture"] [sub_resource type="Resource" id="ChubLakeAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/clam_manila/clam_manila.tres b/fish/species/clam_manila/clam_manila.tres index 20e3f8a..2b3fe73 100644 --- a/fish/species/clam_manila/clam_manila.tres +++ b/fish/species/clam_manila/clam_manila.tres @@ -2,7 +2,7 @@ [ext_resource type="Script" path="res://fish/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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6406.png" id="3_texture"] [resource] script = ExtResource("1_fish_data") diff --git a/fish/species/crab_blue/crab_blue.tres b/fish/species/crab_blue/crab_blue.tres index 2ee3279..b715522 100644 --- a/fish/species/crab_blue/crab_blue.tres +++ b/fish/species/crab_blue/crab_blue.tres @@ -1,9 +1,11 @@ -[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3] +[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3] [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3902.png" id="99_creature_texture"] + [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") allow_night = false @@ -12,7 +14,7 @@ allow_night = false script = ExtResource("1_fish_data") id = &"crab_blue" display_name = "blue crab" -active = false +active = true 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." @@ -30,3 +32,5 @@ weight_max_lb = 1.1 sell_value_min = 3 sell_value_max = 8 sell_value_curve = 0.95 + +display_texture = ExtResource("99_creature_texture") diff --git a/fish/species/crappie_white/crappie_white.tres b/fish/species/crappie_white/crappie_white.tres index 7806c37..6766f1e 100644 --- a/fish/species/crappie_white/crappie_white.tres +++ b/fish/species/crappie_white/crappie_white.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3107.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres b/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres index b53448d..6c055d2 100644 --- a/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres +++ b/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2601.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/croaker_atlantic/croaker_atlantic.tres b/fish/species/croaker_atlantic/croaker_atlantic.tres index c35e7de..d0fb147 100644 --- a/fish/species/croaker_atlantic/croaker_atlantic.tres +++ b/fish/species/croaker_atlantic/croaker_atlantic.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6801.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/croaker_yellowfin/croaker_yellowfin.tres b/fish/species/croaker_yellowfin/croaker_yellowfin.tres index 84885f9..1e282da 100644 --- a/fish/species/croaker_yellowfin/croaker_yellowfin.tres +++ b/fish/species/croaker_yellowfin/croaker_yellowfin.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6807.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres b/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres index 483565c..b758404 100644 --- a/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres +++ b/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/5904.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/goby_round/goby_round.tres b/fish/species/goby_round/goby_round.tres index d8edf5e..3a8ffee 100644 --- a/fish/species/goby_round/goby_round.tres +++ b/fish/species/goby_round/goby_round.tres @@ -3,7 +3,7 @@ [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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3401.png" id="4_texture"] [sub_resource type="Resource" id="RoundGobyAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/goldfish/goldfish.tres b/fish/species/goldfish/goldfish.tres index 444741b..58c7934 100644 --- a/fish/species/goldfish/goldfish.tres +++ b/fish/species/goldfish/goldfish.tres @@ -3,7 +3,7 @@ [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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3002.png" id="4_texture"] [sub_resource type="Resource" id="GoldfishAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/green_sunfish/green_sunfish.tres b/fish/species/green_sunfish/green_sunfish.tres index fdd1ddd..1733035 100644 --- a/fish/species/green_sunfish/green_sunfish.tres +++ b/fish/species/green_sunfish/green_sunfish.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3103.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/grunt_bluestriped/grunt_bluestriped.tres b/fish/species/grunt_bluestriped/grunt_bluestriped.tres index 87e9215..5f247a8 100644 --- a/fish/species/grunt_bluestriped/grunt_bluestriped.tres +++ b/fish/species/grunt_bluestriped/grunt_bluestriped.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/5902.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/grunt_french/grunt_french.tres b/fish/species/grunt_french/grunt_french.tres index da0bd90..8e8161e 100644 --- a/fish/species/grunt_french/grunt_french.tres +++ b/fish/species/grunt_french/grunt_french.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/5903.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres b/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres index 9512381..6d7fe32 100644 --- a/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres +++ b/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6601.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/minnow_fathead/minnow_fathead.tres b/fish/species/minnow_fathead/minnow_fathead.tres index cf00159..c4a3aff 100644 --- a/fish/species/minnow_fathead/minnow_fathead.tres +++ b/fish/species/minnow_fathead/minnow_fathead.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2804.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/porgy_scup/porgy_scup.tres b/fish/species/porgy_scup/porgy_scup.tres index 59dd833..ad96831 100644 --- a/fish/species/porgy_scup/porgy_scup.tres +++ b/fish/species/porgy_scup/porgy_scup.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6904.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/pumpkinseed/pumpkinseed.tres b/fish/species/pumpkinseed/pumpkinseed.tres index 7928e4f..7a4f090 100644 --- a/fish/species/pumpkinseed/pumpkinseed.tres +++ b/fish/species/pumpkinseed/pumpkinseed.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3104.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/roach_common/roach_common.tres b/fish/species/roach_common/roach_common.tres index 8814f1c..c12d380 100644 --- a/fish/species/roach_common/roach_common.tres +++ b/fish/species/roach_common/roach_common.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2504.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/rockfish_black/rockfish_black.tres b/fish/species/rockfish_black/rockfish_black.tres index f26a41c..c37b427 100644 --- a/fish/species/rockfish_black/rockfish_black.tres +++ b/fish/species/rockfish_black/rockfish_black.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6901.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/rudd/rudd.tres b/fish/species/rudd/rudd.tres index ae031ac..5de8d57 100644 --- a/fish/species/rudd/rudd.tres +++ b/fish/species/rudd/rudd.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2508.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/sea_bass_black/sea_bass_black.tres b/fish/species/sea_bass_black/sea_bass_black.tres index 1c0eabd..7a5b567 100644 --- a/fish/species/sea_bass_black/sea_bass_black.tres +++ b/fish/species/sea_bass_black/sea_bass_black.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6902.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/shiner_common/shiner_common.tres b/fish/species/shiner_common/shiner_common.tres index 562daf6..5a8a2f3 100644 --- a/fish/species/shiner_common/shiner_common.tres +++ b/fish/species/shiner_common/shiner_common.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2801.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/shiner_emerald/shiner_emerald.tres b/fish/species/shiner_emerald/shiner_emerald.tres index eeb3653..8a39e5c 100644 --- a/fish/species/shiner_emerald/shiner_emerald.tres +++ b/fish/species/shiner_emerald/shiner_emerald.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2803.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/shiner_golden/shiner_golden.tres b/fish/species/shiner_golden/shiner_golden.tres index 5ab924f..af57441 100644 --- a/fish/species/shiner_golden/shiner_golden.tres +++ b/fish/species/shiner_golden/shiner_golden.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2806.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/smelt_rainbow/smelt_rainbow.tres b/fish/species/smelt_rainbow/smelt_rainbow.tres index 6a0d08a..34691f9 100644 --- a/fish/species/smelt_rainbow/smelt_rainbow.tres +++ b/fish/species/smelt_rainbow/smelt_rainbow.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/4101.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/snapper_lane/snapper_lane.tres b/fish/species/snapper_lane/snapper_lane.tres index 0288e2d..d91444f 100644 --- a/fish/species/snapper_lane/snapper_lane.tres +++ b/fish/species/snapper_lane/snapper_lane.tres @@ -3,7 +3,7 @@ [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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6001.png" id="4_texture"] [sub_resource type="Resource" id="SnapperLaneAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/sunfish/sunfish.tres b/fish/species/sunfish/sunfish.tres index a8e2290..5efd181 100644 --- a/fish/species/sunfish/sunfish.tres +++ b/fish/species/sunfish/sunfish.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/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="Texture2D" path="res://art/exported/creatures/5001.png" id="4_texture"] [resource] script = ExtResource("1_fish_data") diff --git a/fish/species/yellow_perch/yellow_perch.tres b/fish/species/yellow_perch/yellow_perch.tres index 5098a1e..3e0501f 100644 --- a/fish/species/yellow_perch/yellow_perch.tres +++ b/fish/species/yellow_perch/yellow_perch.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="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"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3201.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 2559af5..fdfad53 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -41,8 +41,6 @@ const FishingSurfaceSampleType = preload( const FishingSurfaceResolverType = preload( "res://fishing/fishing_surface_resolver.gd" ) -const ArtShopStockType = preload("res://economy/art_shop_stock.gd") -const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd") const WorldTimeServiceType = preload("res://world/world_time_service.gd") const WorldWeatherServiceType = preload( "res://world/world_weather_service.gd" @@ -134,6 +132,7 @@ const ALTERNATE_REEL_ACTION: StringName = &"reel_alternate" 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 @@ -150,6 +149,7 @@ 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 @@ -222,6 +222,7 @@ func setup( 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 @@ -242,6 +243,7 @@ func setup( _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( @@ -396,6 +398,22 @@ func set_gameplay_input_enabled(enabled: bool) -> void: 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 @@ -530,6 +548,7 @@ 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() ): @@ -542,51 +561,23 @@ func _unhandled_input(event: InputEvent) -> void: 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() return - var selected_item: ItemDataType = _get_active_item() - if ( - state == FishingState.READY - and selected_item != null - and FishingShopStockType.is_gathering_tool(selected_item.item_id) - ): - return if event.is_pressed(): match state: FishingState.READY: if not _new_cast_press_armed: return - var active_item: ItemDataType = _get_active_item() - if ( - active_item != null - and ( - active_item.category == ItemDataType.Category.CONSUMABLE - or ( - active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID - ) - ) - ): - 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 - ) - get_viewport().set_input_as_handled() - return - if not has_active_fishing_rod(): - get_viewport().set_input_as_handled() - return if ( _is_inventory_full() ): @@ -658,6 +649,34 @@ func _unhandled_input(event: InputEvent) -> void: 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") @@ -665,6 +684,10 @@ func _is_reel_input_pressed() -> bool: ) +func is_reel_input_active() -> bool: + return state == FishingState.FIGHTING and _is_reel_input_pressed() + + func _begin_aiming(player: PlayerType) -> void: if state != FishingState.READY or _active_player != null: return @@ -1664,6 +1687,47 @@ func build_network_context( return _build_fishing_context(region) +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 + ): + 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 + + +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 [] diff --git a/gathering/catalog/crab_blue.tres b/gathering/catalog/crab_blue.tres index 1ccabe8..705948e 100644 --- a/gathering/catalog/crab_blue.tres +++ b/gathering/catalog/crab_blue.tres @@ -10,5 +10,8 @@ catch_data = ExtResource("2_catch") required_tool_id = &"crab_net" surface_materials = Array[StringName]([&"sand"]) minimum_surface_y = -0.8 +surface_water_relation = 2 population = 4 +surface_population_per_100_square_meters = 0.4 +maximum_surface_population = 16 sprite_pixel_size = 0.0005 diff --git a/gathering/catalog/crab_brown.tres b/gathering/catalog/crab_brown.tres index 83d20c8..b27bb7e 100644 --- a/gathering/catalog/crab_brown.tres +++ b/gathering/catalog/crab_brown.tres @@ -10,7 +10,10 @@ catch_data = ExtResource("2_catch") required_tool_id = &"crab_net" surface_materials = Array[StringName]([&"sand"]) minimum_surface_y = -0.44 +surface_water_relation = 1 population = 2 +surface_population_per_100_square_meters = 0.15 +maximum_surface_population = 24 movement_speed = 0.35 roam_radius = 3.5 scare_radius = 2.8 diff --git a/gathering/gatherable_data.gd b/gathering/gatherable_data.gd index d60dcc9..41de7fe 100644 --- a/gathering/gatherable_data.gd +++ b/gathering/gatherable_data.gd @@ -10,6 +10,12 @@ enum PresentationMode { WATER_SPURT, } +enum SurfaceWaterRelation { + ANY, + ABOVE_SALT_WATER, + BELOW_SALT_WATER, +} + @export var type_id: StringName @export var catch_data: FishDataType @export var required_tool_id: StringName @@ -17,12 +23,20 @@ enum PresentationMode { @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(-100.0, 100.0, 0.01) var maximum_surface_y: float = 100.0 +@export var surface_water_relation := SurfaceWaterRelation.ANY +@export_range(0.0, 2.0, 0.01) var waterline_margin: float = 0.01 @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 +## Optional density scaling for large generated surfaces. The authored +## population remains the minimum while the maximum keeps large maps bounded. +@export_range(0.0, 8.0, 0.01) +var surface_population_per_100_square_meters := 0.0 +@export_range(0, 256, 1) var maximum_surface_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 @@ -77,6 +91,7 @@ func is_valid() -> bool: or uses_spawn_anchors or not surface_materials.is_empty() ) + and maximum_surface_y >= minimum_surface_y and ( not uses_diggable_area or catch_data.collection_method @@ -87,6 +102,10 @@ func is_valid() -> bool: maximum_anchor_population == 0 or maximum_anchor_population >= population ) + and ( + maximum_surface_population == 0 + or maximum_surface_population >= population + ) and movement_parameters_valid and _quality_multipliers_are_valid( quality_movement_speed_multipliers @@ -126,6 +145,24 @@ func target_population_for_anchor_count(anchor_count: int) -> int: return target +func target_population_for_surface_area(surface_area: float) -> int: + if surface_population_per_100_square_meters <= 0.0: + return population + if surface_area <= 0.0: + return 0 + var target := maxi( + population, + ceili( + surface_area + * surface_population_per_100_square_meters + / 100.0 + ), + ) + if maximum_surface_population > 0: + target = mini(target, maximum_surface_population) + return target + + func can_be_scared() -> bool: return not is_stationary_spawn() and scare_radius > 0.0 diff --git a/gathering/world_gatherable.gd b/gathering/world_gatherable.gd index 8a07ba0..1a71477 100644 --- a/gathering/world_gatherable.gd +++ b/gathering/world_gatherable.gd @@ -29,12 +29,16 @@ func configure( 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: @@ -50,7 +54,6 @@ func configure( ) ) _sprite.rotation_degrees.x = data.sprite_tilt_degrees - apply_network_state(position, yaw, true) func apply_network_state( @@ -147,7 +150,6 @@ func _ensure_water_spurt_visual() -> void: hole.mesh = hole_mesh hole.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _water_spurt_root.add_child(hole) - _emit_water_spurt() func _emit_water_spurt() -> void: @@ -159,6 +161,7 @@ func _emit_water_spurt() -> void: 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 diff --git a/homes/assets/starter_motorcoach.glb b/homes/assets/starter_motorcoach.glb new file mode 100644 index 0000000..becd54e Binary files /dev/null and b/homes/assets/starter_motorcoach.glb differ diff --git a/homes/assets/starter_motorcoach.glb.import b/homes/assets/starter_motorcoach.glb.import new file mode 100644 index 0000000..5cba35b --- /dev/null +++ b/homes/assets/starter_motorcoach.glb.import @@ -0,0 +1,45 @@ +[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/assets/starter_rv_interior.glb b/homes/assets/starter_rv_interior.glb new file mode 100644 index 0000000..21608ea Binary files /dev/null and b/homes/assets/starter_rv_interior.glb differ diff --git a/homes/assets/starter_rv_interior.glb.import b/homes/assets/starter_rv_interior.glb.import new file mode 100644 index 0000000..947870a --- /dev/null +++ b/homes/assets/starter_rv_interior.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b815y7svs3ern" +path="res://.godot/imported/starter_rv_interior.glb-9ad8a345e9e831e82874b4b276341224.scn" + +[deps] + +source_file="res://homes/assets/starter_rv_interior.glb" +dest_files=["res://.godot/imported/starter_rv_interior.glb-9ad8a345e9e831e82874b4b276341224.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/assets/tent_interior.glb b/homes/assets/tent_interior.glb new file mode 100644 index 0000000..3ba2b76 Binary files /dev/null and b/homes/assets/tent_interior.glb differ diff --git a/homes/assets/tent_interior.glb.import b/homes/assets/tent_interior.glb.import new file mode 100644 index 0000000..fc01ba7 --- /dev/null +++ b/homes/assets/tent_interior.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dkfqs051shmxe" +path="res://.godot/imported/tent_interior.glb-576cc47c2c633f7ae635651598663518.scn" + +[deps] + +source_file="res://homes/assets/tent_interior.glb" +dest_files=["res://.godot/imported/tent_interior.glb-576cc47c2c633f7ae635651598663518.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 new file mode 100644 index 0000000..14e5e29 --- /dev/null +++ b/homes/decor_catalog.gd @@ -0,0 +1,59 @@ +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 new file mode 100644 index 0000000..10d2530 --- /dev/null +++ b/homes/decor_catalog.gd.uid @@ -0,0 +1 @@ +uid://bvgwmexas2gps diff --git a/homes/home_decor_controller.gd b/homes/home_decor_controller.gd new file mode 100644 index 0000000..f67207f --- /dev/null +++ b/homes/home_decor_controller.gd @@ -0,0 +1,512 @@ +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 _toolbar != null and _toolbar.visible: + _toolbar.set_interactive(_can_decorate()) + 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 + var inside_home: bool = _is_inside_home_space() + _toolbar.set_active(inside_home) + _toolbar.set_interactive(_can_decorate()) + if not _toolbar.visible: + _clear_selection() + elif not _can_decorate(): + _toolbar.report_status("decor tools pause while another menu is open") + + +func _is_inside_home_space() -> bool: + return ( + _network_home != null + and String(_network_home.get_local_space()).begins_with("rv:") + ) + + +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 new file mode 100644 index 0000000..1778125 --- /dev/null +++ b/homes/home_decor_controller.gd.uid @@ -0,0 +1 @@ +uid://b5j0mqcvhmi54 diff --git a/homes/home_world_service.gd b/homes/home_world_service.gd new file mode 100644 index 0000000..4ec0645 --- /dev/null +++ b/homes/home_world_service.gd @@ -0,0 +1,1504 @@ +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 TENT_INTERIOR_SCENE: PackedScene = preload( + "res://homes/assets/tent_interior.glb" +) +const STARTER_RV_INTERIOR_SCENE: PackedScene = preload( + "res://homes/assets/starter_rv_interior.glb" +) +const HOME_PROMPT_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf") + +const RV_EXTERIOR_BODY_SIZE := Vector3(11.2, 5.0, 4.7) +const RV_EXTERIOR_BODY_CENTER_Y: float = 2.7 +const RV_EXTERIOR_DOOR_LOCAL := Vector3(-0.4, 1.7, 2.96) +const RV_EXTERIOR_SPAWN_LOCAL := Vector3(-0.4, 0.25, 3.95) +const TENT_EXTERIOR_BODY_SIZE := Vector3(4.6, 3.0, 4.6) +const TENT_EXTERIOR_BODY_CENTER_Y: float = 1.5 +const TENT_EXTERIOR_DOOR_LOCAL := Vector3(0.0, 1.0, 2.3) +const TENT_EXTERIOR_SPAWN_LOCAL := Vector3(0.0, 0.25, 3.3) +const RV_EXTERIOR_PROMPT_LOCAL := Vector3(-0.4, 3.72, 3.02) +const TENT_EXTERIOR_PROMPT_LOCAL := Vector3(0.0, 2.48, 2.36) +const HOME_PROMPT_SIZE := Vector2(0.95, 0.28) +const HOME_PROMPT_CORNER_RADIUS: float = 0.08 +const HOME_PROMPT_TEXT_PIXEL_SIZE: float = 0.0045 +const HOME_PROMPT_TEXT_COLOR := Color("c3dfe6") +const HOME_PROMPT_BACKGROUND_COLOR := Color("0a2d3a") +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 TENT_INTERIOR_SIZE := Vector2(4.0, 4.0) +const STARTER_RV_INTERIOR_SIZE := Vector2(4.0, 6.0) +const MIN_EXTERIOR_TRIANGLE_AREA: float = 1.0 +const MIN_EXTERIOR_SEPARATION: float = 14.6 +const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(6.65, 3.73) +const EXTERIOR_SURFACE_INDEX_CELL_SIZE: float = 5.0 +const EXTERIOR_MAX_SURFACE_HEIGHT_DELTA: float = 0.24 +const EXTERIOR_CLEARANCE_HEIGHT: float = 5.5 +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, home_data + ) + 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( + "Home exterior used a dry fallback with obstructed clearance." + ) + return dry_result + var emergency_result: Transform3D = _find_exterior_transform( + owner_peer_id, + triangles, + surface_index, + spawn_transform, + [], + false, + false, + ) + if emergency_result != Transform3D.IDENTITY: + push_warning( + "Home exterior used an accessible emergency placement." + ) + return emergency_result + push_error("No dry, level home exterior footprint was available.") + # The authored spawn is the only universally reachable point. Never strand + # a home on an unchecked elevation when malformed terrain offers no site. + return spawn_transform + + +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) < 9.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 ( + _world != null + and not _world.is_home_exterior_position_accessible( + grounded_position + ) + ): + 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_prompt_local(owner_fingerprint) + ) + + +func set_exterior_prompt_visible( + owner_fingerprint: String, + requested_visible: bool, + action_text: String = "enter home", +) -> void: + for fingerprint: String in _presentations_by_fingerprint: + var presentation: Dictionary = _presentations_by_fingerprint[fingerprint] + var exterior: Node3D = presentation.get("exterior") + if not is_instance_valid(exterior): + continue + var prompt := exterior.get_node_or_null("DoorPrompt") as Node3D + if prompt == null: + continue + prompt.visible = ( + requested_visible + and fingerprint == owner_fingerprint + and not _dedicated_runtime + ) + if prompt.visible: + var label := prompt.get_node_or_null("Message") as Label3D + if label != null: + label.text = action_text + + +func get_exterior_spawn_transform(owner_fingerprint: String) -> Transform3D: + var exterior_transform: Transform3D = get_exterior_transform( + owner_fingerprint + ) + # Exterior local +Z leads away from the doorway. Player visuals face local + # -Z, so turn the authoritative spawn basis around to start the day facing + # outward instead of looking back into the home. + var outward_spawn_basis: Basis = ( + exterior_transform.basis * Basis(Vector3.UP, PI) + ) + return Transform3D( + outward_spawn_basis, + exterior_transform * _exterior_spawn_local(owner_fingerprint), + ) + + +func get_interior_spawn_transform( + owner_fingerprint: String, + visitor_offset: int = 0, +) -> Transform3D: + # `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) + var exit_direction: Vector3 = get_interior_exit_direction(owner_fingerprint) + var lane_offset: float = ( + float(posmod(visitor_slot, 4)) - 1.5 + ) * 0.28 + var row_offset: float = ( + float(floori(float(visitor_slot) / 4.0)) * 0.28 + ) + # Spawn just through the authored doorway. Local presentation then walks the + # avatar farther into the room before returning movement control. + var spawn_origin: Vector3 = ( + get_interior_exit_position(owner_fingerprint) + + exit_direction * 0.42 + - exit_direction * row_offset + + Vector3(0.0, 0.0, lane_offset) + ) + spawn_origin.y = get_interior_anchor(owner_fingerprint).y + 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(owner_fingerprint) + ) + 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 has_avatar_crossed_interior_exit_threshold( + avatar: Player, + owner_fingerprint: String, +) -> bool: + if avatar == null: + return false + var local_position: Vector3 = interior_global_to_local( + owner_fingerprint, + avatar.global_position, + ) + var threshold_position: Vector3 = interior_global_to_local( + owner_fingerprint, + get_interior_exit_position(owner_fingerprint), + ) + var exit_direction: Vector3 = get_interior_exit_direction( + owner_fingerprint + ) + return ( + (local_position - threshold_position).dot(exit_direction) >= 0.0 + and absf(local_position.z - threshold_position.z) <= 0.85 + and local_position.y >= -0.25 + and local_position.y <= 2.25 + ) + + +func get_interior_exit_direction(owner_fingerprint: String) -> Vector3: + return ( + Vector3.LEFT + if _home_tier(owner_fingerprint) + >= PlayerHomeStateType.STARTER_RV_TIER + else Vector3.RIGHT + ) + + +func get_interior_exit_position(owner_fingerprint: String) -> Vector3: + var tier: int = _home_tier(owner_fingerprint) + return get_interior_anchor(owner_fingerprint) + ( + Vector3(-STARTER_RV_INTERIOR_SIZE.x * 0.5 + 0.6, 0.35, 0.0) + if tier >= PlayerHomeStateType.STARTER_RV_TIER + else Vector3(TENT_INTERIOR_SIZE.x * 0.5 - 0.55, 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(fingerprint) + ) + if distance <= closest_distance: + closest = fingerprint + closest_distance = distance + return closest + + +func _home_tier(owner_fingerprint: String) -> int: + var presentation: Dictionary = _presentations_by_fingerprint.get( + owner_fingerprint, {} + ) + var home_data: Dictionary = presentation.get("home", {}) + return int(home_data.get("tier", PlayerHomeStateType.STARTER_TIER)) + + +func _exterior_door_local(owner_fingerprint: String) -> Vector3: + return ( + RV_EXTERIOR_DOOR_LOCAL + if _home_tier(owner_fingerprint) + >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_EXTERIOR_DOOR_LOCAL + ) + + +func _exterior_spawn_local(owner_fingerprint: String) -> Vector3: + return ( + RV_EXTERIOR_SPAWN_LOCAL + if _home_tier(owner_fingerprint) + >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_EXTERIOR_SPAWN_LOCAL + ) + + +func _exterior_prompt_local(owner_fingerprint: String) -> Vector3: + return ( + RV_EXTERIOR_PROMPT_LOCAL + if _home_tier(owner_fingerprint) + >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_EXTERIOR_PROMPT_LOCAL + ) + + +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, + home_data: Dictionary, +) -> Node3D: + var root := Node3D.new() + root.name = "Home_%s" % fingerprint.left(8) + _exteriors_root.add_child(root) + root.global_transform = world_transform + var tier: int = int(home_data.get( + "tier", PlayerHomeStateType.STARTER_TIER + )) + if tier >= PlayerHomeStateType.STARTER_RV_TIER: + 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", + RV_EXTERIOR_BODY_SIZE, + Vector3(0.0, RV_EXTERIOR_BODY_CENTER_Y, 0.0), + ) + else: + if not _dedicated_runtime: + _add_tent_exterior(root) + _add_box_collision( + root, + "Body", + TENT_EXTERIOR_BODY_SIZE, + Vector3(0.0, TENT_EXTERIOR_BODY_CENTER_Y, 0.0), + ) + if not _dedicated_runtime: + _add_exterior_prompt(root, tier) + 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 = "HomeInterior_%s" % fingerprint.left(8) + root.position = INTERIOR_ORIGIN + Vector3( + float(interior_index) * INTERIOR_SPACING, 0.0, 0.0 + ) + _interiors_root.add_child(root) + var tier: int = int(home_data.get( + "tier", PlayerHomeStateType.STARTER_TIER + )) + var scene: PackedScene = ( + STARTER_RV_INTERIOR_SCENE + if tier >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_INTERIOR_SCENE + ) + var shell: Node3D = scene.instantiate() as Node3D + if shell == null: + push_error("The home interior shell could not be instantiated.") + else: + shell.name = "InteriorShell" + root.add_child(shell) + _prepare_imported_interior(shell) + 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 _add_tent_exterior(parent: Node3D) -> void: + var tent := MeshInstance3D.new() + tent.name = "StarterTent" + var mesh := CylinderMesh.new() + mesh.top_radius = 0.0 + mesh.bottom_radius = 3.05 + mesh.height = 3.0 + mesh.radial_segments = 4 + mesh.rings = 1 + mesh.cap_top = false + mesh.material = _material(Color("9a7049")) + tent.mesh = mesh + tent.position.y = 1.5 + tent.rotation.y = PI * 0.25 + parent.add_child(tent) + _add_box( + parent, + "TentDoor", + Vector3(1.25, 1.85, 0.04), + Vector3(0.0, 0.95, 2.18), + Color("173b32"), + false, + ) + + +func _add_exterior_prompt(parent: Node3D, tier: int) -> void: + var prompt := Node3D.new() + prompt.name = "DoorPrompt" + prompt.position = ( + RV_EXTERIOR_PROMPT_LOCAL + if tier >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_EXTERIOR_PROMPT_LOCAL + ) + prompt.visible = false + parent.add_child(prompt) + + var background := MeshInstance3D.new() + background.name = "Background" + background.mesh = _rounded_rectangle_mesh( + HOME_PROMPT_SIZE, + HOME_PROMPT_CORNER_RADIUS, + ) + background.material_override = _prompt_material( + HOME_PROMPT_BACKGROUND_COLOR + ) + prompt.add_child(background) + + var label := Label3D.new() + label.name = "Message" + label.text = "enter home" + label.position.z = 0.012 + label.font = HOME_PROMPT_FONT + label.font_size = 28 + label.pixel_size = HOME_PROMPT_TEXT_PIXEL_SIZE + label.modulate = HOME_PROMPT_TEXT_COLOR + label.outline_size = 0 + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + label.width = HOME_PROMPT_SIZE.x / HOME_PROMPT_TEXT_PIXEL_SIZE + label.double_sided = true + prompt.add_child(label) + + +func _rounded_rectangle_mesh(size: Vector2, radius: float) -> ArrayMesh: + var half_size: Vector2 = size * 0.5 + var clamped_radius: float = minf( + radius, + minf(half_size.x, half_size.y), + ) + var corners: Array[Vector2] = [ + Vector2(half_size.x - clamped_radius, half_size.y - clamped_radius), + Vector2(-half_size.x + clamped_radius, half_size.y - clamped_radius), + Vector2(-half_size.x + clamped_radius, -half_size.y + clamped_radius), + Vector2(half_size.x - clamped_radius, -half_size.y + clamped_radius), + ] + var start_angles: Array[float] = [0.0, PI * 0.5, PI, PI * 1.5] + var perimeter: Array[Vector2] = [] + const CORNER_SEGMENTS: int = 4 + for corner_index: int in corners.size(): + for segment_index: int in range(CORNER_SEGMENTS + 1): + var angle: float = start_angles[corner_index] + ( + PI * 0.5 * float(segment_index) / float(CORNER_SEGMENTS) + ) + perimeter.append( + corners[corner_index] + + Vector2(cos(angle), sin(angle)) * clamped_radius + ) + var surface := SurfaceTool.new() + surface.begin(Mesh.PRIMITIVE_TRIANGLES) + for index: int in perimeter.size(): + var first: Vector2 = perimeter[index] + var second: Vector2 = perimeter[(index + 1) % perimeter.size()] + for point: Vector2 in [Vector2.ZERO, first, second]: + surface.set_normal(Vector3.FORWARD) + surface.add_vertex(Vector3(point.x, point.y, 0.0)) + return surface.commit() + + +func _prompt_material(color: Color) -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.albedo_color = color + material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + material.roughness = 1.0 + material.metallic = 0.0 + material.cull_mode = BaseMaterial3D.CULL_DISABLED + return material + + +func _prepare_imported_interior(shell: Node3D) -> void: + for value: Node in shell.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := value as MeshInstance3D + if mesh_instance == null or mesh_instance.mesh == null: + continue + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var source_material: Material = mesh_instance.get_active_material( + surface_index + ) + var material := source_material as StandardMaterial3D + if material == null: + continue + var flat_material := material.duplicate() as StandardMaterial3D + flat_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + flat_material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST + flat_material.roughness = 1.0 + flat_material.metallic = 0.0 + mesh_instance.set_surface_override_material( + surface_index, flat_material + ) + _add_imported_mesh_collision(mesh_instance) + + +func _add_imported_mesh_collision(mesh_instance: MeshInstance3D) -> void: + var parent: Node = mesh_instance.get_parent() + if parent == null or mesh_instance.mesh == null: + return + var shape: ConcavePolygonShape3D = mesh_instance.mesh.create_trimesh_shape() + if shape == null: + return + var body := StaticBody3D.new() + body.name = "%sCollision" % mesh_instance.name + body.transform = mesh_instance.transform + var collision := CollisionShape3D.new() + collision.shape = shape + body.add_child(collision) + parent.add_child(body) + + +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 := active_interior.find_child( + "%sCollision" % wall_name, true, false + ) as StaticBody3D + 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 tier: int = int((presentation.get("home", {}) as Dictionary).get( + "tier", PlayerHomeStateType.STARTER_TIER + )) + var interior_size: Vector2 = ( + STARTER_RV_INTERIOR_SIZE + if tier >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_INTERIOR_SIZE + ) + var hidden_walls: Dictionary[String, bool] = {} + var outside_horizontal_wall: bool = ( + absf(local_camera.x) >= interior_size.x * 0.5 - 0.05 + ) + var outside_depth_wall: bool = ( + absf(local_camera.z) >= interior_size.y * 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 := interior.find_child( + wall_name, true, false + ) as MeshInstance3D + if wall != null: + wall.visible = not hidden_walls.has(wall_name) + for child: Node in interior.find_children( + "%s*" % wall_name, "GeometryInstance3D", true, false + ): + 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 new file mode 100644 index 0000000..5505d06 --- /dev/null +++ b/homes/home_world_service.gd.uid @@ -0,0 +1 @@ +uid://cy2kjoxhwxujd diff --git a/homes/player_home_state.gd b/homes/player_home_state.gd new file mode 100644 index 0000000..a89d6a3 --- /dev/null +++ b/homes/player_home_state.gd @@ -0,0 +1,841 @@ +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_tent" +const STARTER_RV_TIER: int = 1 +const STARTER_RV_EXTERIOR_ID: StringName = &"starter_motorcoach" +const MAX_TIER: int = STARTER_RV_TIER +const HOME_UPGRADE_COSTS: Array[int] = [50] +const STORAGE_CAPACITIES: Array[int] = [9, 18] +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 +const TENT_INTERIOR_HALF_EXTENTS := Vector2(1.91, 1.91) +const STARTER_RV_INTERIOR_HALF_EXTENTS := Vector2(1.91, 2.91) +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_storage_capacity() -> int: + return storage_capacity_for_tier(_tier) + + +func get_next_upgrade_cost() -> int: + if _tier >= MAX_TIER: + return -1 + return HOME_UPGRADE_COSTS[_tier] + + +func get_next_storage_capacity() -> int: + if _tier >= MAX_TIER: + return -1 + return storage_capacity_for_tier(_tier + 1) + + +func upgrade_to_next_tier() -> bool: + if _tier >= MAX_TIER: + return false + _tier += 1 + _exterior_id = exterior_id_for_tier(_tier) + _touch() + return true + + +static func exterior_id_for_tier(tier: int) -> StringName: + return ( + STARTER_RV_EXTERIOR_ID + if tier >= STARTER_RV_TIER + else STARTER_EXTERIOR_ID + ) + + +static func storage_capacity_for_tier(tier: int) -> int: + return STORAGE_CAPACITIES[clampi(tier, STARTER_TIER, MAX_TIER)] + + +static func interior_half_extents_for_tier(tier: int) -> Vector2: + return ( + STARTER_RV_INTERIOR_HALF_EXTENTS + if tier >= STARTER_RV_TIER + else TENT_INTERIOR_HALF_EXTENTS + ) + + +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, _tier + ) + 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 = exterior_id_for_tier(_tier) + _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 {} + exterior_id = String(exterior_id_for_tier(tier)) + 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, tier) + 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, + home_tier: int = STARTER_TIER, +) -> 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, + home_tier, + ) + 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, + home_tier: int = STARTER_TIER, +) -> 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 interior_half_extents := interior_half_extents_for_tier(home_tier) + 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, + _tier, + ) + 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, + _tier, + ) + 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 new file mode 100644 index 0000000..37836c6 --- /dev/null +++ b/homes/player_home_state.gd.uid @@ -0,0 +1 @@ +uid://ggfdoak2ixxx diff --git a/inventory/player_bag.gd b/inventory/player_bag.gd index e33f1e4..d94845d 100644 --- a/inventory/player_bag.gd +++ b/inventory/player_bag.gd @@ -60,6 +60,28 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool: 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) if item == null or quantity <= 0: diff --git a/inventory/player_hotbar.gd b/inventory/player_hotbar.gd index b7d5197..36068ea 100644 --- a/inventory/player_hotbar.gd +++ b/inventory/player_hotbar.gd @@ -314,6 +314,25 @@ 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, diff --git a/inventory/player_inventory_layout.gd b/inventory/player_inventory_layout.gd index 244e10b..bce726e 100644 --- a/inventory/player_inventory_layout.gd +++ b/inventory/player_inventory_layout.gd @@ -37,6 +37,7 @@ 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 @@ -72,7 +73,44 @@ 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 _first_free_slot(InventoryContainer.INVENTORY) >= 0 + 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: @@ -332,6 +370,7 @@ func restore_from_save_data(data: Dictionary) -> bool: "container": container, "slot": slot, } + _prepared_item_placements.clear() _placements = restored _reconcile(true) return true @@ -340,6 +379,7 @@ func restore_from_save_data(data: Dictionary) -> bool: 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: @@ -402,8 +442,17 @@ func _reconcile(force_signal: bool) -> void: 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 := _first_free_slot(target_container) + 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) @@ -484,6 +533,37 @@ func _first_free_slot(container: InventoryContainer) -> int: 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] diff --git a/items/catalog/basic_cube.tres b/items/catalog/basic_cube.tres new file mode 100644 index 0000000..86a2cf9 --- /dev/null +++ b/items/catalog/basic_cube.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_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/item_catalog.tres b/items/catalog/item_catalog.tres index 63ef3ea..2f15e0a 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=41 format=3] +[gd_resource type="Resource" script_class="ItemCatalog" load_steps=42 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"] @@ -40,7 +40,8 @@ [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"] [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")] +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")] diff --git a/main/main.gd b/main/main.gd index 1724bd9..324c268 100644 --- a/main/main.gd +++ b/main/main.gd @@ -37,6 +37,12 @@ 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 RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) @@ -70,6 +76,7 @@ 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" ) @@ -123,6 +130,14 @@ const GatheringControllerType = preload( 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 = ( @@ -136,6 +151,13 @@ 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 +const HOME_ENTRY_INPUT_OWNER: StringName = &"home_entry_transition" +const HOME_EXIT_INPUT_OWNER: StringName = &"home_exit_transition" +const HOME_EXIT_FADE_GENERATION: int = -104729 +const HOME_ENTRY_WALK_DISTANCE: float = 1.05 +const HOME_ENTRY_WALK_DURATION: float = 0.55 +const HOME_EXIT_WALK_DISTANCE: float = 1.25 +const HOME_EXIT_WALK_DURATION: float = 0.62 @export var fish_catalog: FishPoolType @export var pelican_buyer_profile: FishBuyerProfileType @@ -153,6 +175,7 @@ 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 @@ -212,6 +235,10 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 ) @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 ) @@ -234,6 +261,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 @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 @@ -241,6 +269,8 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 var _gameplay_started: bool = false var _save_slots := PlayerSaveSlotCatalogType.new() var _shop_interaction: FishingShopInteractionType +var _decor_shop_interaction: DecorShopInteractionType +var _rv_upgrade_shop_interaction: RVUpgradeInteractionType var _storage_interaction: PlayerStorageInteractionType var _title_music_tween: Tween var _title_music_transition_generation: int = 0 @@ -271,12 +301,19 @@ var _pending_existing_root_path := "" var _local_recovery_attempt_id: String = "" var _dedicated_runtime: bool = false var _dedicated_metrics_accumulator: float = 0.0 +var _home_exit_transition_active: bool = false +var _home_exit_waiting_for_network: bool = false +var _home_exit_start_position: Vector3 = Vector3.ZERO +var _home_exit_walk_tween: Tween +var _home_entry_transition_active: bool = false +var _home_entry_walk_tween: Tween @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: @@ -294,7 +331,7 @@ func _ready() -> void: _configure_presentation_performance_profile( _performance_profile.is_light() ) - DisplayServer.window_set_title("NETfishing") + DisplayServer.window_set_title("straywild") _rain_ambience = RainAmbienceType.new() _rain_ambience.name = "RainAmbience" add_child(_rain_ambience) @@ -324,11 +361,12 @@ func _ready() -> void: func _configure_presentation_performance_profile(light_profile: bool) -> void: - _game_ui.get_title_screen().set_minimal_presentation(light_profile) _game_ui.set_light_performance_profile(light_profile) - var title_water_material := _title_background.material as ShaderMaterial - if title_water_material != null: - title_water_material.set_shader_parameter( + 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, ) @@ -461,7 +499,7 @@ func _print_dedicated_metrics() -> void: metrics["gatherables"] = _network_world_spawns.call( "get_network_metrics" ) - print("NETfishing server metrics: %s" % JSON.stringify(metrics)) + print("straywild server metrics: %s" % JSON.stringify(metrics)) func _start_dedicated_server() -> void: @@ -470,10 +508,7 @@ func _start_dedicated_server() -> void: _fail_dedicated_server(config.error_message) return var data_path: String = config.data_directory - var manifest_path: String = data_path.path_join( - PlayerDataRoot.MANIFEST_FILENAME - ) - if not FileAccess.file_exists(manifest_path): + 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( @@ -556,7 +591,7 @@ func _start_dedicated_server() -> void: _fail_dedicated_server(_discovery.get_host_status_message()) return print( - "NETfishing dedicated server ready: %s on %s:%d (%d players, %s, timezone %s)" + "straywild dedicated server ready: %s on %s:%d (%d players, %s, timezone %s)" % [ config.server_name, config.bind_address, @@ -725,6 +760,7 @@ func _initialize_application(dedicated: bool) -> void: _player.wallet, _asset_reservations ) + _player.cooler_capacity.bind_home_state(_home_state) _player.bag.setup(item_catalog) _player.inventory_layout.setup( _player.bag, @@ -741,14 +777,24 @@ func _initialize_application(dedicated: bool) -> void: _player.inventory_layout, ) _shop_interaction = _test_world.get_fishing_shop() + _decor_shop_interaction = _test_world.get_decor_shop() + _rv_upgrade_shop_interaction = _test_world.get_rv_upgrade_shop() _storage_interaction = _test_world.get_player_storage() if not dedicated: + if not _home_state.changed.is_connected(_on_home_state_changed): + _home_state.changed.connect(_on_home_state_changed) _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() + _refresh_shop_npc_speech_state() + _decor_shop_interaction.setup_local_player(_player) + _decor_shop_interaction.local_player_range_changed.connect( + _on_decor_shop_range_changed + ) + _rv_upgrade_shop_interaction.setup_local_player(_player) + _rv_upgrade_shop_interaction.local_player_range_changed.connect( + _on_rv_upgrade_shop_range_changed ) _storage_interaction.setup_local_player(_player) _storage_interaction.local_player_range_changed.connect( @@ -771,6 +817,23 @@ func _initialize_application(dedicated: bool) -> void: _world_weather, _player_jobs, _player, + _home_state, + ) + _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) @@ -904,6 +967,10 @@ func _initialize_application(dedicated: bool) -> void: _save_manager, _asset_reservations, _player.inventory_layout, + _home_state, + _network_home, + _decor_shop_interaction, + _rv_upgrade_shop_interaction, ) _fishing_spot.setup( _player, @@ -921,6 +988,7 @@ func _initialize_application(dedicated: bool) -> void: _network_fishing, _world_time, _world_weather, + Callable(_test_world, "get_fishable_water_regions"), ) if dedicated: return @@ -942,11 +1010,14 @@ func _initialize_application(dedicated: bool) -> void: _player.fishing_upgrades, _shop_interaction, _storage_interaction, + _decor_shop_interaction, + _rv_upgrade_shop_interaction, _player.item_effects, _player.cooler_capacity, _network_session, _network_sale, _network_shop, + _home_state, _network_chat, _network_profile, _player_spawn_service, @@ -964,6 +1035,27 @@ func _initialize_application(dedicated: bool) -> void: _test_world.get_world_environment(), _test_world.get_sun(), ) + _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) + _network_home.local_transition_finished.connect( + _on_local_home_transition_finished + ) + _game_ui.get_screen_fade().transition_completed.connect( + _on_home_exit_fade_transition_completed + ) + _game_ui.rv_storage_requested.connect(_on_rv_storage_requested) _game_ui.setup_data_and_identity( _data_root, _identity_backups, @@ -972,6 +1064,7 @@ func _initialize_application(dedicated: bool) -> void: _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) @@ -1006,7 +1099,8 @@ func _initialize_application(dedicated: bool) -> void: _game_ui, _game_ui.get_screen_fade(), _test_world.get_player_water_triggers(), - _test_world.get_safe_respawn_points() + _test_world.get_safe_respawn_points(), + Callable(_test_world, "get_water_recovery_position"), ) _ui_pixelation.effective_pixel_size_changed.connect( _on_effective_ui_pixel_size_changed @@ -1096,7 +1190,7 @@ func _initialize_application(dedicated: bool) -> void: func _show_data_root_setup() -> void: if _data_setup_dialog == null: _data_setup_dialog = ConfirmationDialog.new() - _data_setup_dialog.title = "NETfishing player data" + _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) @@ -1113,14 +1207,14 @@ func _show_data_root_setup() -> void: var default_path: String = _data_root.default_visible_path() var legacy: bool = PortableDataMigration.legacy_files_present() _data_setup_dialog.dialog_text = ( - ("Move your existing NETfishing data to an easy-to-find folder." - if legacy else "Choose where NETfishing stores your player data.") + ("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/NETFISHING" + "Move to Documents/straywild" if legacy else "Use This Folder" ) if not default_path.is_empty() @@ -1129,7 +1223,7 @@ func _show_data_root_setup() -> void: _data_setup_current_button.visible = not default_path.is_empty() if not _data_root.error_message.is_empty(): _data_setup_dialog.dialog_text = ( - "Your NETfishing data folder is unavailable.\n\n%s" + "Your straywild data folder is unavailable.\n\n%s" % _data_root.error_message ) _data_setup_dialog.popup_centered(Vector2i(640, 360)) @@ -1265,11 +1359,11 @@ func _activate_selected_data_path(path: String, app_data: bool = false) -> void: func _show_existing_root_choice(path: String) -> void: _pending_existing_root_path = path var dialog: ConfirmationDialog = ConfirmationDialog.new() - dialog.title = "Existing NETfishing data" + 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 NETfishing player data.\n\n" + "This folder already contains straywild player data.\n\n" + "Choose which complete data set to use. Data is never merged automatically." ) dialog.add_button( @@ -1343,6 +1437,18 @@ func _on_data_root_status(message: String) -> void: _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, @@ -1461,6 +1567,12 @@ func _input(event: InputEvent) -> void: 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 _game_ui.get_rv_upgrade_shop().consume_escape(): + get_viewport().set_input_as_handled() + return if fishing_shop.consume_escape(): get_viewport().set_input_as_handled() return @@ -1559,19 +1671,54 @@ func _is_pause_open_request(event: InputEvent) -> bool: 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 ( + _network_home.can_local_decorate() + and _fishing_spot.can_open_fishing_shop() + and _game_ui.get_player_storage().open_storage_from_home(true) + ): + _refresh_home_door_prompt({}) + 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): + _refresh_home_door_prompt({}) + get_viewport().set_input_as_handled() + return if ( - _storage_interaction != null - and _storage_interaction.is_local_player_in_range() + _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() and _fishing_spot.can_open_fishing_shop() - and _game_ui.get_player_storage().open_storage() + and _game_ui.get_rv_upgrade_shop().open_shop() ): - _game_ui.set_storage_prompt_visible(false) + _game_ui.set_shop_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 ( @@ -1594,37 +1741,48 @@ func _process(delta: float) -> void: ) _print_dedicated_metrics() return - var show_shop_prompt := _can_show_shop_prompt() + _maybe_begin_home_exit_transition() + var show_rv_upgrade_prompt := _can_show_rv_upgrade_shop_prompt() + var show_decor_shop_prompt := _can_show_decor_shop_prompt() + var show_shop_prompt := ( + show_rv_upgrade_prompt + or show_decor_shop_prompt + or _can_show_shop_prompt() + ) var shop_prompt_anchor := ( - _shop_interaction.get_prompt_anchor_position() - if _shop_interaction != null - else Vector3.ZERO + _rv_upgrade_shop_interaction.get_prompt_anchor_position() + if show_rv_upgrade_prompt + else ( + _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 show_storage_prompt := _can_show_storage_prompt() - var storage_prompt_anchor := ( - _storage_interaction.get_prompt_anchor_position() - if _storage_interaction != null - else Vector3.ZERO - ) - _game_ui.set_storage_prompt_visible( - show_storage_prompt, - storage_prompt_anchor, + var home_prompt := _get_home_prompt() + _refresh_home_door_prompt(home_prompt) + _game_ui.set_rv_storage_button_visible( + _can_show_rv_storage_button() ) func _apply_runtime_settings(settings: PlayerSettingsType) -> void: if settings == null: return - var requested_window_mode: DisplayServer.WindowMode = ( - DisplayServer.WINDOW_MODE_FULLSCREEN - if settings.fullscreen_enabled - else DisplayServer.WINDOW_MODE_WINDOWED + var current_window_mode: DisplayServer.WindowMode = ( + DisplayServer.window_get_mode() ) - if DisplayServer.window_get_mode() != requested_window_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) @@ -1651,6 +1809,25 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void: ) +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 @@ -2332,17 +2509,9 @@ func _on_remote_recovery_requested( peer_id, "Fishing attempt ended." ) - var target_position: Vector3 = _test_world.get_player_spawn_transform().origin - var nearest_distance: float = INF - for point: SafeRespawnPoint in _test_world.get_safe_respawn_points(): - if point == null or not point.enabled: - continue - var distance: float = point.get_horizontal_distance_squared( - entry_position - ) - if distance < nearest_distance: - nearest_distance = distance - target_position = point.global_position + 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 @@ -2415,11 +2584,13 @@ func _apply_world( 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, @@ -2443,10 +2614,30 @@ func _refresh_active_world_bindings() -> void: _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 + ) + if is_instance_valid(_rv_upgrade_shop_interaction): + if _rv_upgrade_shop_interaction.local_player_range_changed.is_connected( + _on_rv_upgrade_shop_range_changed + ): + _rv_upgrade_shop_interaction.local_player_range_changed.disconnect( + _on_rv_upgrade_shop_range_changed + ) _shop_interaction = _test_world.get_fishing_shop() + _decor_shop_interaction = _test_world.get_decor_shop() + _rv_upgrade_shop_interaction = _test_world.get_rv_upgrade_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) + _network_shop.set_rv_upgrade_shop_interaction( + _rv_upgrade_shop_interaction + ) if _dedicated_runtime: return if _shop_interaction != null: @@ -2459,9 +2650,21 @@ func _refresh_active_world_bindings() -> void: _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 + ) + if _rv_upgrade_shop_interaction != null: + _rv_upgrade_shop_interaction.setup_local_player(_player) + _rv_upgrade_shop_interaction.local_player_range_changed.connect( + _on_rv_upgrade_shop_range_changed + ) _game_ui.set_world_interactions( _shop_interaction, _storage_interaction, + _decor_shop_interaction, + _rv_upgrade_shop_interaction, ) _on_shop_range_changed( _shop_interaction != null @@ -2471,6 +2674,14 @@ func _refresh_active_world_bindings() -> void: _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() + ) + _on_rv_upgrade_shop_range_changed( + _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + ) func _on_water_recovery_starting() -> void: @@ -2484,6 +2695,8 @@ func _on_water_recovery_starting() -> void: _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_rv_upgrade_shop().close_for_water_recovery() _game_ui.get_player_storage().close_for_water_recovery() @@ -2532,6 +2745,7 @@ func _on_active_hotbar_item_changed( and item != null and item.is_available() and _player.bag.owns_item(item_id) + and not String(_network_home.get_local_space()).begins_with("rv:") ) var active_is_catching_net: bool = ( item_id == FishingShopStockType.CRAB_NET_ID @@ -2545,6 +2759,12 @@ func _on_active_hotbar_item_changed( 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, @@ -2555,6 +2775,7 @@ func _on_active_hotbar_item_changed( ) _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) @@ -2674,6 +2895,8 @@ func _finish_quit() -> void: func _exit_tree() -> void: + _cancel_home_entry_transition() + _cancel_home_exit_transition(true) if _dedicated_runtime and _network_session != null: _network_session.disconnect_session("Dedicated server stopping.") if _title_music_tween != null: @@ -2691,16 +2914,224 @@ func _exit_tree() -> void: func _on_shop_range_changed(in_range: bool) -> void: - _game_ui.set_shop_npc_player_in_range(in_range) + _refresh_shop_npc_speech_state() if not in_range: _game_ui.get_fishing_shop().close_for_range_exit() - _game_ui.set_shop_prompt_visible(_can_show_shop_prompt()) + _game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt()) func _on_storage_range_changed(in_range: bool) -> void: if not in_range: _game_ui.get_player_storage().close_for_range_exit() - _game_ui.set_storage_prompt_visible(_can_show_storage_prompt()) + var prompt := _get_home_prompt() + _refresh_home_door_prompt(prompt) + _game_ui.set_rv_storage_button_visible( + _can_show_rv_storage_button() + ) + + +func _on_decor_shop_range_changed(in_range: bool) -> void: + _refresh_shop_npc_speech_state() + if not in_range: + _game_ui.get_decor_shop().close_for_range_exit() + _game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt()) + + +func _on_rv_upgrade_shop_range_changed(in_range: bool) -> void: + _refresh_shop_npc_speech_state() + if not in_range: + _game_ui.get_rv_upgrade_shop().close_for_range_exit() + _game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt()) + + +func _on_home_state_changed() -> void: + _refresh_shop_npc_speech_state() + + +func _on_local_home_space_changed( + space_id: StringName, + owner_fingerprint: String, +) -> void: + var inside_rv: bool = String(space_id).begins_with("rv:") + _refresh_active_hotbar_item() + _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() + ) + if inside_rv and not _home_exit_transition_active: + _begin_home_entry_transition(owner_fingerprint) + elif not inside_rv and _home_entry_transition_active: + _cancel_home_entry_transition() + + +func _begin_home_entry_transition(owner_fingerprint: String) -> void: + if owner_fingerprint.is_empty() or _home_entry_transition_active: + return + _home_entry_transition_active = true + _refresh_home_door_prompt({}) + _player.set_local_input_suppressed(HOME_ENTRY_INPUT_OWNER, true) + _player.set_camera_input_suppressed(HOME_ENTRY_INPUT_OWNER, true) + var inward_direction: Vector3 = ( + -_home_world.get_interior_exit_direction(owner_fingerprint) + ) + _player.face_world_direction(inward_direction) + _player.begin_animation_action(PlayerType.CHARACTER_WALKING_ANIMATION) + _home_entry_walk_tween = create_tween() + _home_entry_walk_tween.set_trans(Tween.TRANS_SINE) + _home_entry_walk_tween.set_ease(Tween.EASE_IN_OUT) + _home_entry_walk_tween.tween_property( + _player, + "global_position", + _player.global_position + inward_direction * HOME_ENTRY_WALK_DISTANCE, + HOME_ENTRY_WALK_DURATION, + ) + _home_entry_walk_tween.finished.connect( + _cancel_home_entry_transition, + CONNECT_ONE_SHOT, + ) + + +func _cancel_home_entry_transition() -> void: + if _home_entry_walk_tween != null and _home_entry_walk_tween.is_valid(): + _home_entry_walk_tween.kill() + _home_entry_walk_tween = null + if is_instance_valid(_player): + _player.end_animation_action() + _player.set_local_input_suppressed(HOME_ENTRY_INPUT_OWNER, false) + _player.set_camera_input_suppressed(HOME_ENTRY_INPUT_OWNER, false) + _home_entry_transition_active = false + + +func _maybe_begin_home_exit_transition() -> void: + if ( + _home_exit_transition_active + or _home_entry_transition_active + or not _gameplay_started + or _water_recovery.is_recovery_active() + or not _fishing_spot.can_open_fishing_shop() + or _game_ui.get_player_storage().visible + or _game_ui.get_fishing_shop().visible + or _game_ui.get_decor_shop().visible + or _game_ui.get_rv_upgrade_shop().visible + ): + return + var owner_fingerprint: String = ( + _network_home.get_local_home_owner_fingerprint() + ) + if ( + owner_fingerprint.is_empty() + or not _home_world.has_avatar_crossed_interior_exit_threshold( + _player, + owner_fingerprint, + ) + ): + return + _begin_home_exit_transition(owner_fingerprint) + + +func _begin_home_exit_transition(owner_fingerprint: String) -> void: + _home_exit_transition_active = true + _home_exit_waiting_for_network = false + _home_exit_start_position = _player.global_position + _refresh_home_door_prompt({}) + _player.set_local_input_suppressed(HOME_EXIT_INPUT_OWNER, true) + _player.set_camera_input_suppressed(HOME_EXIT_INPUT_OWNER, true) + var exit_direction: Vector3 = _home_world.get_interior_exit_direction( + owner_fingerprint + ) + _player.face_world_direction(exit_direction) + _player.begin_animation_action(PlayerType.CHARACTER_WALKING_ANIMATION) + _home_exit_walk_tween = create_tween() + _home_exit_walk_tween.set_trans(Tween.TRANS_SINE) + _home_exit_walk_tween.set_ease(Tween.EASE_IN_OUT) + _home_exit_walk_tween.tween_property( + _player, + "global_position", + _player.global_position + exit_direction * HOME_EXIT_WALK_DISTANCE, + HOME_EXIT_WALK_DURATION, + ) + _home_exit_walk_tween.finished.connect( + _on_home_exit_walk_finished, + CONNECT_ONE_SHOT, + ) + + +func _on_home_exit_walk_finished() -> void: + _home_exit_walk_tween = null + if not _home_exit_transition_active: + return + # End the walking action on the same frame translation ends. The fade and + # authoritative relocation may take longer, but a stationary avatar must + # never keep cycling its walk animation during that wait. + _player.end_animation_action() + _game_ui.get_screen_fade().fade_to_black(HOME_EXIT_FADE_GENERATION) + + +func _on_home_exit_fade_transition_completed( + generation: int, + faded_to_black: bool, +) -> void: + if ( + generation != HOME_EXIT_FADE_GENERATION + or not _home_exit_transition_active + ): + return + if faded_to_black: + # A local host can apply the authoritative transition synchronously from + # request_exit(). Mark this first so its completion signal is not lost. + _home_exit_waiting_for_network = true + var request_started: bool = _network_home.request_exit() + if not request_started: + _home_exit_waiting_for_network = false + _player.global_position = _home_exit_start_position + _game_ui.get_screen_fade().fade_from_black( + HOME_EXIT_FADE_GENERATION + ) + return + _cancel_home_exit_transition(false) + + +func _on_local_home_transition_finished( + accepted: bool, + _message: String, +) -> void: + if ( + not _home_exit_transition_active + or not _home_exit_waiting_for_network + ): + return + _home_exit_waiting_for_network = false + if not accepted: + _player.global_position = _home_exit_start_position + _game_ui.get_screen_fade().fade_from_black(HOME_EXIT_FADE_GENERATION) + + +func _cancel_home_exit_transition(reset_fade: bool) -> void: + if _home_exit_walk_tween != null and _home_exit_walk_tween.is_valid(): + _home_exit_walk_tween.kill() + _home_exit_walk_tween = null + if is_instance_valid(_player): + _player.end_animation_action() + _player.set_local_input_suppressed(HOME_EXIT_INPUT_OWNER, false) + _player.set_camera_input_suppressed(HOME_EXIT_INPUT_OWNER, false) + if ( + reset_fade + and is_instance_valid(_game_ui) + and is_instance_valid(_game_ui.get_screen_fade()) + ): + _game_ui.get_screen_fade().reset_immediately() + _home_exit_transition_active = false + _home_exit_waiting_for_network = false + _home_exit_start_position = Vector3.ZERO + + +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: @@ -2709,18 +3140,141 @@ func _can_show_shop_prompt() -> bool: 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 _game_ui.get_rv_upgrade_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 _game_ui.get_rv_upgrade_shop().visible + and not _water_recovery.is_recovery_active() + and _fishing_spot.can_open_fishing_shop() + ) + + +func _can_show_rv_upgrade_shop_prompt() -> bool: + return ( + _gameplay_started + and _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + and not _game_ui.get_rv_upgrade_shop().visible + 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_any_shop_prompt() -> bool: + return ( + _can_show_shop_prompt() + or _can_show_decor_shop_prompt() + or _can_show_rv_upgrade_shop_prompt() + ) + + +func _is_any_shop_npc_in_range() -> bool: + return ( + ( + _shop_interaction != null + and _shop_interaction.is_local_player_in_range() + ) + or ( + _decor_shop_interaction != null + and _decor_shop_interaction.is_local_player_in_range() + ) + or ( + _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + ) + ) + + +func _refresh_shop_npc_speech_state() -> void: + var interaction := _get_active_shop_npc_interaction() + var dialogue_text: String = "" + if interaction != null: + dialogue_text = interaction.get_dialogue_text(_home_state.get_tier()) + _game_ui.set_shop_npc_player_in_range( + interaction != null, + dialogue_text, + ) + + +func _get_active_shop_npc_interaction() -> FishingShopInteractionType: + # Match the interaction and prompt priority used above: RV, decor, fishing. + if ( + _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + ): + return _rv_upgrade_shop_interaction + if ( + _decor_shop_interaction != null + and _decor_shop_interaction.is_local_player_in_range() + ): + return _decor_shop_interaction + if ( + _shop_interaction != null + and _shop_interaction.is_local_player_in_range() + ): + return _shop_interaction + return null + + func _can_show_storage_prompt() -> bool: + return not _get_home_prompt().is_empty() + + +func _refresh_home_door_prompt(prompt: Dictionary) -> void: + # Home entry belongs to the doorway in world space. Keep the retired HUD + # prompt hidden so it cannot overlap the compact door-mounted sign. + _game_ui.set_storage_prompt_visible(false) + _home_world.set_exterior_prompt_visible( + str(prompt.get("owner_fingerprint", "")), + not prompt.is_empty(), + str(prompt.get("label", "enter home")), + ) + + +func _can_show_rv_storage_button() -> bool: return ( _gameplay_started - and _storage_interaction != null - and _storage_interaction.is_local_player_in_range() - and not _game_ui.get_player_storage().visible - and not _game_ui.get_fishing_shop().visible + 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 _game_ui.get_rv_upgrade_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(): + return {} + var nearby_owner: String = _home_world.find_nearby_exterior_owner(_player) + if nearby_owner.is_empty(): + return {} + return { + "label": "enter home", + "anchor": _home_world.get_exterior_prompt_position(nearby_owner), + "owner_fingerprint": nearby_owner, + } diff --git a/main/main.tscn b/main/main.tscn index b9ad6fb..778a1d4 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -38,7 +38,7 @@ [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="Shader" uid="uid://c8s1g7tn771du" path="res://ui/title_water_background.gdshader" id="40_title_water"] +[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"] @@ -58,19 +58,17 @@ [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_water_native"] -shader = ExtResource("40_title_water") -shader_parameter/surface_color = Color(0.1, 0.45, 0.62, 1) -shader_parameter/middle_color = Color(0.055, 0.285, 0.4, 1) -shader_parameter/depth_color = Color(0.02, 0.16, 0.27, 1) -shader_parameter/virtual_pixel_density = Vector2(320, 180) -shader_parameter/color_step_count = 14.0 -shader_parameter/continuous_depth_blend = 0.12 -shader_parameter/wave_speed = 0.1 -shader_parameter/wave_scale = 2.4 -shader_parameter/wave_strength = 0.012 -shader_parameter/caustic_strength = 0.018 +[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") @@ -139,7 +137,7 @@ layer = -1 [node name="TitleBackground" type="ColorRect" parent="TitleBackgroundLayer" unique_id=1768697365] unique_name_in_owner = true -material = SubResource("ShaderMaterial_title_water_native") +material = SubResource("ShaderMaterial_title_grass_native") anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -265,6 +263,14 @@ script = ExtResource("22_network_sale") 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") @@ -328,6 +334,10 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.3999481, 12.07081) [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") @@ -344,6 +354,10 @@ script = ExtResource("8_recovery") 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] unique_name_in_owner = true script = ExtResource("10_settings") diff --git a/main/runtime_performance_profile.gd b/main/runtime_performance_profile.gd index ee2581d..f18e63a 100644 --- a/main/runtime_performance_profile.gd +++ b/main/runtime_performance_profile.gd @@ -2,9 +2,13 @@ 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 LEGACY_LIGHT_ENVIRONMENT_VARIABLE: String = "NETFISHING_LOW_END" +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 @@ -14,9 +18,19 @@ 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(OS.get_environment(PROFILE_ENVIRONMENT_VARIABLE)), - OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1", + StringName(profile_name), + ( + OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1" + or OS.get_environment(NETFISHING_LIGHT_ENVIRONMENT_VARIABLE) == "1" + ), ) diff --git a/main/texture_sampling_policy.gd b/main/texture_sampling_policy.gd index 3d041d0..35a92a0 100644 --- a/main/texture_sampling_policy.gd +++ b/main/texture_sampling_policy.gd @@ -1,6 +1,6 @@ extends Node -## Enforces NETfishing's nearest-neighbor-only artwork policy at runtime. +## 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, @@ -15,8 +15,12 @@ func _enter_tree() -> void: func _on_node_added(node: Node) -> void: enforce_node(node) # Some runtime presenters assign their mesh or material immediately after - # add_child(). Recheck once deferred so those resources are covered too. - call_deferred("_enforce_deferred_node", weakref(node)) + # 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: @@ -29,6 +33,15 @@ func _enforce_deferred_node(node_ref: WeakRef) -> void: 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(): diff --git a/network/discovery_client.gd b/network/discovery_client.gd index 31b8054..d8e602a 100644 --- a/network/discovery_client.gd +++ b/network/discovery_client.gd @@ -20,12 +20,13 @@ signal friend_invite_received( signal friend_invite_finished(success: bool, message: String) const BASE_URL_SETTING: String = "network/discovery/base_url" -const BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_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 = "NETfishing Room" +const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room" const LEGACY_DEDICATED_DEFAULT_ROOM_NAME: String = ( - "NETfishing Dedicated Server" + "straywild Dedicated Server" ) const ROOM_NAME_SUFFIX: String = "'s Server" const MAX_ROOM_NAME_LENGTH: int = 48 @@ -33,7 +34,7 @@ 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 = "NETFISHING_TRAVERSAL_V1 " +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 @@ -553,6 +554,10 @@ func is_own_room(room: Dictionary) -> bool: 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("/"): @@ -560,7 +565,7 @@ func _configured_base_url() -> String: if not value.is_empty() and not ( value.begins_with("https://") or value.begins_with("http://") ): - push_warning("Ignored invalid NETfishing discovery URL.") + push_warning("Ignored invalid straywild discovery URL.") return "" return value @@ -809,7 +814,7 @@ func _on_traversal_request_completed( var port: int = int(endpoint.get("port", 0)) if address.is_empty() or port < 1 or port > 65535: continue - var punch: PackedByteArray = "NETFISHING_PUNCH_V1".to_utf8_buffer() + var punch: PackedByteArray = "straywild_PUNCH_V1".to_utf8_buffer() for _attempt: int in 3: _session.send_traversal_packet(address, port, punch) @@ -895,13 +900,13 @@ func _configure_upnp_mapping(port: int) -> Dictionary: var mapping_result: int = upnp.add_port_mapping( port, port, - "NETfishing", + "straywild", "UDP", UPNP_MAPPING_DURATION_SECONDS, ) if mapping_result == UPNP.UPNP_RESULT_ONLY_PERMANENT_LEASE_SUPPORTED: mapping_result = upnp.add_port_mapping( - port, port, "NETfishing", "UDP", 0 + port, port, "straywild", "UDP", 0 ) return { "success": mapping_result == UPNP.UPNP_RESULT_SUCCESS, @@ -1374,18 +1379,18 @@ func _discovery_version_mismatch_message(required_version: String) -> String: match rejection: NetworkProtocol.RejectionCode.CLIENT_OUTDATED: return ( - "Your NETfishing version is out of date. Update to %s to " + "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 NETfishing %s. This room " + "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 NETfishing %s. This room will " + "Public discovery requires straywild %s. This room will " + "not be listed until both versions match." ) % required_version @@ -1577,4 +1582,4 @@ func _save_settings() -> void: config.set_value("social", "share_presence", _presence_sharing) var error: Error = config.save(SETTINGS_PATH) if error != OK: - push_warning("Could not save local NETfishing discovery settings.") + push_warning("Could not save local straywild discovery settings.") diff --git a/network/host_ban_store.gd b/network/host_ban_store.gd index f8c4bf9..1251846 100644 --- a/network/host_ban_store.gd +++ b/network/host_ban_store.gd @@ -17,6 +17,10 @@ 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: diff --git a/network/identity_backup_service.gd b/network/identity_backup_service.gd index ff7396c..c383e7b 100644 --- a/network/identity_backup_service.gd +++ b/network/identity_backup_service.gd @@ -3,7 +3,8 @@ extends Node signal operation_finished(success: bool, message: String) -const MAGIC := "NETFISHING_IDENTITY_BACKUP" +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 @@ -110,8 +111,9 @@ func inspect_backup( 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") != MAGIC + 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 @@ -134,12 +136,23 @@ func inspect_backup( var signature: PackedByteArray = Marshalls.base64_to_raw( str(data.get("self_signature", "")) ) - if not NetworkIdentityCrypto.verify_fields( - NetworkIdentityCrypto.load_public_key(public_pem), - "identity_backup_self_test", - [expected_type, fingerprint], - 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] @@ -189,7 +202,7 @@ func import_backup( ) _finish( bool(result.get("ok", false)), - "Identity imported. Restart NETfishing before multiplayer." + "Identity imported. Restart straywild before multiplayer." if bool(result.get("ok", false)) else "Could not install this identity backup.", ) diff --git a/network/known_player_store.gd b/network/known_player_store.gd index 0c6b859..1118656 100644 --- a/network/known_player_store.gd +++ b/network/known_player_store.gd @@ -15,6 +15,10 @@ 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: diff --git a/network/network_home_protocol.gd b/network/network_home_protocol.gd new file mode 100644 index 0000000..96756e9 --- /dev/null +++ b/network/network_home_protocol.gd @@ -0,0 +1,188 @@ +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 new file mode 100644 index 0000000..24b6aae --- /dev/null +++ b/network/network_home_protocol.gd.uid @@ -0,0 +1 @@ +uid://fl3qwiluejhm diff --git a/network/network_home_service.gd b/network/network_home_service.gd new file mode 100644 index 0000000..4885961 --- /dev/null +++ b/network/network_home_service.gd @@ -0,0 +1,1450 @@ +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 _authorized_home_tiers: Dictionary[int, int] = {} +var _home_upgrade_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 get_authoritative_home_tier(peer_id: int) -> int: + if _authorized_home_tiers.has(peer_id): + return int(_authorized_home_tiers[peer_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", {}) + return int(home.get("tier", -1)) + + +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 authorize_home_upgrade( + peer_id: int, + resulting_tier: int, + result_id: String, +) -> void: + if ( + not _session.is_host() + or peer_id <= 0 + or result_id.is_empty() + or resulting_tier < PlayerHomeStateType.STARTER_TIER + or resulting_tier > PlayerHomeStateType.MAX_TIER + ): + return + _authorized_home_tiers[peer_id] = resulting_tier + _home_upgrade_result_owner[result_id] = peer_id + + +func finalize_home_upgrade( + peer_id: int, + result_id: String, + applied: bool, +) -> void: + if _home_upgrade_result_owner.get(result_id, 0) != peer_id: + return + _home_upgrade_result_owner.erase(result_id) + if not applied: + _authorized_home_tiers.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 home 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 home 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 home 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 home.") + 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"] + var prior_tier: int = int(prior_home["tier"]) + var next_tier: int = int(home_data["tier"]) + var tier_is_authorized: bool = ( + next_tier != prior_tier + and int(_authorized_home_tiers.get(peer_id, -1)) == next_tier + ) + if ( + int(home_data["revision"]) <= int(prior_home["revision"]) + or (next_tier != prior_tier and not tier_is_authorized) + or str(home_data["exterior_id"]) + != str(PlayerHomeStateType.exterior_id_for_tier(next_tier)) + ): + 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: + if ( + _authorized_home_tiers.has(peer_id) + and int(home_data["tier"]) == int(_authorized_home_tiers[peer_id]) + ): + _authorized_home_tiers.erase(peer_id) + 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 home 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 home 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 home.") + 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 home 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) + _authorized_home_tiers.erase(peer_id) + for result_id: String in _home_upgrade_result_owner.keys(): + if int(_home_upgrade_result_owner[result_id]) == peer_id: + _home_upgrade_result_owner.erase(result_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() + _authorized_home_tiers.clear() + _home_upgrade_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 new file mode 100644 index 0000000..be233ed --- /dev/null +++ b/network/network_home_service.gd.uid @@ -0,0 +1 @@ +uid://ong8kug36ku7 diff --git a/network/network_identity_crypto.gd b/network/network_identity_crypto.gd index 4886cca..9d4f09c 100644 --- a/network/network_identity_crypto.gd +++ b/network/network_identity_crypto.gd @@ -5,7 +5,8 @@ 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 = "NETFISHING" +const DOMAIN_PREFIX: String = "straywild" +const LEGACY_DOMAIN_PREFIX: String = "NETFISHING" const IDENTITY_VERSION: String = "identity_v1" @@ -50,8 +51,16 @@ static func secure_id(byte_count: int = 16) -> String: 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, DOMAIN_PREFIX) + _append_string(output, prefix) _append_string(output, IDENTITY_VERSION) _append_string(output, domain) for value: Variant in fields: @@ -117,6 +126,37 @@ static func verify_fields( ) +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: diff --git a/network/network_item_use_service.gd b/network/network_item_use_service.gd index cf2d5f9..024eca9 100644 --- a/network/network_item_use_service.gd +++ b/network/network_item_use_service.gd @@ -5,6 +5,7 @@ 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 @@ -394,6 +395,10 @@ func _apply_equipped(data: Dictionary) -> void: 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"]) ) diff --git a/network/network_profile_preferences.gd b/network/network_profile_preferences.gd index 3c4996b..b6cac63 100644 --- a/network/network_profile_preferences.gd +++ b/network/network_profile_preferences.gd @@ -21,6 +21,8 @@ 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: diff --git a/network/network_profile_service.gd b/network/network_profile_service.gd index e4732a9..ff967f1 100644 --- a/network/network_profile_service.gd +++ b/network/network_profile_service.gd @@ -51,6 +51,17 @@ func setup( _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 diff --git a/network/network_protocol.gd b/network/network_protocol.gd index 6bbbf9b..5b3ca89 100644 --- a/network/network_protocol.gd +++ b/network/network_protocol.gd @@ -3,7 +3,7 @@ extends RefCounted const WorldLayoutType = preload("res://world/world_layout.gd") -const PROTOCOL_VERSION: int = 10 +const PROTOCOL_VERSION: int = 12 const GAME_BUILD: String = "prealpha" const MAX_GAME_VERSION_LENGTH: int = 64 const MAX_DISPLAY_NAME_LENGTH: int = 24 @@ -17,9 +17,9 @@ const MAX_SIGNATURE_LENGTH: int = 2048 # 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, and 16 reliable -# fishing lifecycle. Bulk world/art payloads must not head-of-line block held -# items, fishing, or animation presentation. +# 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 @@ -30,7 +30,8 @@ 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 ENET_CHANNEL_COUNT: int = 17 +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" @@ -45,6 +46,8 @@ 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 HOME_UPGRADE_CAPABILITY: String = "home_upgrade_v1" const DEFAULT_WORLD_SEED: int = 13001 const MAX_WORLD_SEED: int = 2147483646 @@ -202,6 +205,8 @@ static func make_client_hello( FRIENDS_CAPABILITY, MOVEMENT_RECONCILIATION_CAPABILITY, FISHING_REPLICATION_CAPABILITY, + HOME_CAPABILITY, + HOME_UPGRADE_CAPABILITY, ]), "cosmetic_snapshot": cosmetic_snapshot, "identity_fingerprint": identity_fingerprint, @@ -304,7 +309,7 @@ static func make_server_hello( assigned_peer_id: int, player_count: int, max_players: int, - server_display_name: String = "NETfishing", + server_display_name: String = "straywild", world_seed: int = DEFAULT_WORLD_SEED, world_layout: StringName = WorldLayoutType.GENERATED, ) -> Dictionary: @@ -342,6 +347,8 @@ static func make_server_hello( FRIENDS_CAPABILITY, MOVEMENT_RECONCILIATION_CAPABILITY, FISHING_REPLICATION_CAPABILITY, + HOME_CAPABILITY, + HOME_UPGRADE_CAPABILITY, "chat_v1", "mail_v1", "profile_v1", @@ -372,17 +379,17 @@ static func rejection_text(code: int) -> String: return "You are not permitted to join this server." RejectionCode.CLIENT_OUTDATED: return ( - "Your NETfishing version is out of date. " + "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 NETfishing." + + "The host needs to update straywild." ) RejectionCode.VERSION_MISMATCH: return ( - "This game and server use different NETfishing versions. " + "This game and server use different straywild versions. " + "Update both to the latest release." ) _: diff --git a/network/network_session.gd b/network/network_session.gd index 2711976..7c259e0 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -28,6 +28,7 @@ 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 @@ -124,6 +125,9 @@ 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] = {} @@ -141,6 +145,7 @@ 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 @@ -160,7 +165,7 @@ var _local_appearance_snapshot: Dictionary = ( ) var _moderation_disconnect_message := "" var _host_port: int = 0 -var _session_display_name: String = "NETfishing Room" +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] = {} @@ -334,6 +339,8 @@ func _register_player_host() -> void: NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, + NetworkProtocol.HOME_CAPABILITY, + NetworkProtocol.HOME_UPGRADE_CAPABILITY, ]), ) _registry.update_appearance(1, _local_appearance_snapshot) @@ -432,7 +439,7 @@ func set_peer_operator( func set_session_display_name(value: String) -> void: var cleaned: String = value.strip_edges().left(48) if cleaned.is_empty(): - cleaned = "NETfishing Room" + cleaned = "straywild Room" if cleaned == _session_display_name: return _session_display_name = cleaned @@ -661,6 +668,8 @@ func supports_server_capability(capability: StringName) -> bool: NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, + NetworkProtocol.HOME_CAPABILITY, + NetworkProtocol.HOME_UPGRADE_CAPABILITY, "chat_v1", "mail_v1", "profile_v1", @@ -685,6 +694,55 @@ 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 @@ -1030,6 +1088,7 @@ func _on_peer_disconnected(peer_id: int) -> void: _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, "") @@ -1187,7 +1246,7 @@ func resolve_server_trust(accepted: bool) -> void: if not _server_trust.trust( get_current_endpoint(), str(_pending_server_proof["server_fingerprint"]), - "NETfishing", + "straywild", ): _fail_identity("The server identity could not be pinned.") return @@ -1313,6 +1372,7 @@ func submit_client_hello(data: Dictionary) -> void: 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( @@ -1546,6 +1606,7 @@ func receive_server_hello(data: Dictionary) -> void: 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() @@ -1578,6 +1639,8 @@ func receive_server_hello(data: Dictionary) -> void: NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, + NetworkProtocol.HOME_CAPABILITY, + NetworkProtocol.HOME_UPGRADE_CAPABILITY, ]), ) _registry.update_appearance(local_peer_id, _local_appearance_snapshot) @@ -1623,6 +1686,7 @@ 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) @@ -2247,6 +2311,8 @@ func _broadcast_movement_snapshots() -> void: 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 @@ -2315,7 +2381,7 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void: _animation_refresh_accumulator, ANIMATION_REFRESH_INTERVAL, ) - var updates: Array = [] + 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: @@ -2333,12 +2399,18 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void: _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.append(encoded) - if updates.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() @@ -2538,6 +2610,8 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void: 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)) ) @@ -2555,6 +2629,33 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void: ) +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() @@ -2883,11 +2984,15 @@ func _teardown_peer() -> void: _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() diff --git a/network/network_shop_protocol.gd b/network/network_shop_protocol.gd index 2de9cc1..e58556e 100644 --- a/network/network_shop_protocol.gd +++ b/network/network_shop_protocol.gd @@ -4,6 +4,8 @@ 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 HOME_UPGRADE_CAPABILITY: StringName = &"home_upgrade_v1" const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL const MAX_ID_LENGTH: int = 96 const MAX_MESSAGE_LENGTH: int = 160 @@ -14,10 +16,11 @@ enum ProductCategory { ROD, REEL_SPEED_UPGRADE, BARRIER_POWER_UPGRADE, - COOLER_CAPACITY_UPGRADE, + HOME_UPGRADE, ART_KIT, ART_UPGRADE, BACKPACK_CAPACITY_UPGRADE, + DECOR, } diff --git a/network/network_shop_service.gd b/network/network_shop_service.gd index 6f31c40..e4d6f9c 100644 --- a/network/network_shop_service.gd +++ b/network/network_shop_service.gd @@ -8,11 +8,14 @@ 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 SHOP_ID: StringName = &"main_fishing_shop" +const FISHING_SHOP_ID: StringName = &"main_fishing_shop" +const DECOR_SHOP_ID: StringName = &"rv_decor_shop" +const RV_UPGRADE_SHOP_ID: StringName = &"rv_upgrade_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 HOME_UPGRADE_PRODUCT_ID: StringName = &"starter_rv_upgrade" const BACKPACK_PRODUCT_ID: StringName = &"backpack_capacity_upgrade" const MAX_LEDGER_ENTRIES_PER_PEER: int = 64 @@ -31,6 +34,8 @@ var _session: NetworkSession var _spawn_service: PlayerSpawnService var _network_fishing: NetworkFishingService var _interaction: FishingShopInteraction +var _decor_interaction: DecorShopInteraction +var _rv_upgrade_interaction: FishingShopInteraction var _wallet: PlayerWallet var _bag: PlayerBag var _item_catalog: ItemCatalog @@ -47,6 +52,8 @@ 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( @@ -63,6 +70,10 @@ func setup( save_manager: PlayerSaveManager, reservations: PlayerAssetReservationService, inventory_layout: PlayerInventoryLayout = null, + home_state: PlayerHomeState = null, + network_home: NetworkHomeService = null, + decor_interaction: DecorShopInteraction = null, + rv_upgrade_interaction: FishingShopInteraction = null, ) -> void: _session = session _spawn_service = spawn_service @@ -77,6 +88,10 @@ func setup( _save_manager = save_manager _reservations = reservations _inventory_layout = inventory_layout + _home_state = home_state + _network_home = network_home + _decor_interaction = decor_interaction + _rv_upgrade_interaction = rv_upgrade_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): @@ -87,6 +102,18 @@ func set_shop_interaction(interaction: FishingShopInteraction) -> void: _interaction = interaction +func set_decor_shop_interaction( + interaction: DecorShopInteraction, +) -> void: + _decor_interaction = interaction + + +func set_rv_upgrade_shop_interaction( + interaction: FishingShopInteraction, +) -> void: + _rv_upgrade_interaction = interaction + + func can_request_purchase() -> bool: return ( _session != null @@ -124,6 +151,32 @@ func can_request_backpack_purchase() -> bool: ) +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 can_request_home_upgrade() -> bool: + return ( + can_request_purchase() + and _home_state != null + and ( + _session.is_host() + or _session.supports_server_capability( + NetworkShopProtocol.HOME_UPGRADE_CAPABILITY + ) + ) + ) + + func is_local_purchase_pending() -> bool: return not _pending_local_request.is_empty() @@ -174,12 +227,12 @@ func request_barrier_power_upgrade() -> String: ) -func request_cooler_capacity_upgrade() -> String: +func request_home_upgrade() -> String: return _request_purchase( - COOLER_PRODUCT_ID, - NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE, + HOME_UPGRADE_PRODUCT_ID, + NetworkShopProtocol.ProductCategory.HOME_UPGRADE, 1, - _cooler_capacity.get_level() if _cooler_capacity != null else 0 + _home_state.get_tier() if _home_state != null else 0 ) @@ -193,6 +246,15 @@ func request_backpack_capacity_upgrade() -> String: ) +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, @@ -269,11 +331,30 @@ func _request_purchase( product_id, category, 0, 0, ) return "" + if ( + category == NetworkShopProtocol.ProductCategory.HOME_UPGRADE + and not can_request_home_upgrade() + ): + local_purchase_finished.emit( + "", false, "RV 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 = _shop_id_for_category(category) var request: Dictionary = { "request_id": request_id, "session_id": _session.get_session_id(), - "shop_id": str(SHOP_ID), + "shop_id": str(shop_id), "product_id": str(product_id), "category": category, "quantity": quantity, @@ -345,16 +426,38 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void: _rejected_result(data, "A purchase is already pending.") ) return - if not _is_shop_available_for_peer(peer_id): + 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 = ( + _interaction_for_shop(requested_shop_id) + ) var message: String = ( "The shop is unavailable." - if avatar == null or _interaction == null + 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 _network_home != null + ): + match int(result.get("category", -1)): + NetworkShopProtocol.ProductCategory.DECOR: + _network_home.authorize_decor_purchase( + peer_id, + StringName(str(result["product_id"])), + int(result["resulting_state"]), + str(result["result_id"]), + ) + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: + _network_home.authorize_home_upgrade( + peer_id, + int(result["resulting_state"]), + str(result["result_id"]), + ) _pending_by_peer[peer_id] = request_id _record_and_send(peer_id, result) @@ -368,10 +471,29 @@ func _build_authoritative_result( 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 + elif ( + category == NetworkShopProtocol.ProductCategory.HOME_UPGRADE + and _network_home != null + ): + var authoritative_tier: int = ( + _network_home.get_authoritative_home_tier(peer_id) + ) + if authoritative_tier >= 0: + current_state = authoritative_tier var cost: int = -1 var resulting_state: int = current_state var rejection: String = "" - if StringName(str(request["shop_id"])) != SHOP_ID: + var expected_shop_id: StringName = _shop_id_for_category(category) + if StringName(str(request["shop_id"])) != expected_shop_id: rejection = "The shop is unavailable." elif quantity < 1: rejection = "Purchase could not be completed." @@ -484,18 +606,14 @@ func _build_authoritative_result( ] ) resulting_state = current_state + 1 - NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE: + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: if ( - product_id != COOLER_PRODUCT_ID - or current_state >= PlayerCoolerCapacity.MAX_LEVEL + product_id != HOME_UPGRADE_PRODUCT_ID + or current_state >= PlayerHomeState.MAX_TIER ): rejection = "Upgrade is already at maximum." else: - cost = ( - PlayerCoolerCapacity.EXPANSION_COSTS[ - current_state - ] - ) + cost = PlayerHomeState.HOME_UPGRADE_COSTS[current_state] resulting_state = current_state + 1 NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE: if ( @@ -545,6 +663,19 @@ func _build_authoritative_result( 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(): @@ -667,6 +798,9 @@ func _apply_purchase_result(data: Dictionary) -> void: 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) @@ -676,6 +810,8 @@ func _apply_purchase_result(data: Dictionary) -> void: 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.") @@ -696,6 +832,7 @@ func _validate_local_result(data: Dictionary) -> String: 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", "")) @@ -770,10 +907,12 @@ func _validate_local_result(data: Dictionary) -> String: 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: + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: + if ( + product_id != HOME_UPGRADE_PRODUCT_ID + or _home_state.get_tier() != expected_state + or _home_state.get_next_upgrade_cost() != cost + ): return "Purchase could not be completed." NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE: if ( @@ -806,6 +945,20 @@ func _validate_local_result(data: Dictionary) -> String: 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 "" @@ -829,8 +982,11 @@ func _apply_local_product(data: Dictionary) -> bool: 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.HOME_UPGRADE: + return ( + _wallet.debit(int(data["total_cost"])) + and _home_state.upgrade_to_next_tier() + ) NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE: return ( _inventory_layout != null @@ -845,6 +1001,12 @@ func _apply_local_product(data: Dictionary) -> bool: 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 @@ -921,7 +1083,7 @@ func _handle_acknowledgement( peer_id: int, result_id: String, request_id: String, - _applied: bool, + applied: bool, message: String, ) -> void: if ( @@ -933,20 +1095,31 @@ func _handle_acknowledgement( or _result_owners.get(result_id, 0) != peer_id ): return + if _network_home != null: + _network_home.finalize_decor_purchase( + peer_id, result_id, applied + ) + _network_home.finalize_home_upgrade( + 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) -> bool: +func _is_shop_available_for_peer( + peer_id: int, + shop_id: StringName, +) -> bool: + var interaction: FishingShopInteraction = _interaction_for_shop(shop_id) 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) + or interaction == null + or not is_instance_valid(interaction) ): return false var avatar: Player = _spawn_service.get_avatar(peer_id) @@ -957,10 +1130,28 @@ func _is_shop_available_for_peer(peer_id: int) -> bool: _network_fishing == null or not _network_fishing.has_peer_attempt(peer_id) ) - and _interaction.is_avatar_in_range(avatar) + and interaction.is_avatar_in_range(avatar) ) +func _shop_id_for_category(category: int) -> StringName: + match category: + NetworkShopProtocol.ProductCategory.DECOR: + return DECOR_SHOP_ID + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: + return RV_UPGRADE_SHOP_ID + return FISHING_SHOP_ID + + +func _interaction_for_shop(shop_id: StringName) -> FishingShopInteraction: + match shop_id: + DECOR_SHOP_ID: + return _decor_interaction + RV_UPGRADE_SHOP_ID: + return _rv_upgrade_interaction + return _interaction + + func _on_peer_removed(peer_id: int) -> void: _request_ledgers.erase(peer_id) _pending_by_peer.erase(peer_id) diff --git a/network/network_surface_drawing_service.gd b/network/network_surface_drawing_service.gd index 4edc234..485dfaa 100644 --- a/network/network_surface_drawing_service.gd +++ b/network/network_surface_drawing_service.gd @@ -250,7 +250,11 @@ func activate( _pointer_screen_position = _clamped_pointer_position( initial_pointer_position ) - _prior_mouse_mode = Input.mouse_mode + # 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() @@ -266,6 +270,10 @@ func deactivate() -> void: _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 = "" @@ -273,10 +281,24 @@ func deactivate() -> void: _aim_hit.clear() _hide_previews() _refresh_stencil_visibility() - Input.mouse_mode = _prior_mouse_mode + 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 @@ -879,7 +901,7 @@ func export_canvas_png(canvas_id: String) -> String: _emit_hud_state("paint something before exporting") return "" if _data_root == null or _data_root.root_path.is_empty(): - _emit_hud_state("choose a NETfishing data folder before exporting") + _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: @@ -990,7 +1012,7 @@ func _stamp_entry_is_unlocked(entry: Dictionary) -> bool: func _next_artwork_export_path(directory: String) -> String: var date_time: Dictionary = Time.get_datetime_dict_from_system() - var stem: String = "netfishing-artwork-%04d%02d%02d-%02d%02d%02d" % [ + 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)), diff --git a/network/network_world_spawn_service.gd b/network/network_world_spawn_service.gd index b27dd80..8981237 100644 --- a/network/network_world_spawn_service.gd +++ b/network/network_world_spawn_service.gd @@ -43,6 +43,7 @@ var _entity_revisions: Dictionary = {} var _surface_triangles: Dictionary = {} var _surface_areas: Dictionary = {} var _surface_total_areas: Dictionary = {} +var _surface_height_ranges: Dictionary = {} var _spawn_anchor_positions: Dictionary = {} var _respawns: Array[Dictionary] = [] var _next_respawn_by_type: Dictionary[StringName, float] = {} @@ -322,10 +323,14 @@ func _cache_spawn_surface(entry: GatherableDataType) -> void: if not entry.diggable_area_id.is_empty(): triangles = _world.get_diggable_area_triangles(entry.diggable_area_id) else: + var height_range := _resolved_surface_height_range(entry) triangles = _world.get_spawn_surface_triangles( entry.surface_materials, - entry.minimum_surface_y, + height_range.x, + 0.6, + height_range.y, ) + _surface_height_ranges[entry.type_id] = height_range var areas := PackedFloat32Array() var total_area: float = 0.0 for triangle: PackedVector3Array in triangles: @@ -518,7 +523,21 @@ func _sample_surface_position( ) if triangles.is_empty() or total_area <= 0.0: return Vector3(INF, INF, INF) - var attempts: int = 48 if is_finite(maximum_distance) else 1 + var validates_diggable_surface := not entry.diggable_area_id.is_empty() + var height_range: Vector2 = _surface_height_ranges.get( + entry.type_id, + Vector2(entry.minimum_surface_y, entry.maximum_surface_y), + ) + var validates_height_range := _surface_height_ranges.has(entry.type_id) + var attempts: int = ( + 48 + if ( + validates_diggable_surface + or validates_height_range + 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 @@ -534,12 +553,33 @@ func _sample_surface_position( + triangle[2] * barycentric_c ) fallback = point - if ( + 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, + ) + ) + var valid_height := ( + not validates_height_range + or ( + point.y > height_range.x + and point.y < height_range.y + ) + ) + if ( + within_requested_distance + and valid_diggable_surface + and valid_height ): return point + if validates_diggable_surface or validates_height_range: + return Vector3(INF, INF, INF) if origin.is_finite() and is_finite(maximum_distance): return origin return fallback @@ -1390,8 +1430,12 @@ func _reconcile_seasonal_population() -> void: 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 + if entry == null: + return 0 + if entry.spawn_anchor_set_id.is_empty(): + return entry.target_population_for_surface_area( + float(_surface_total_areas.get(entry.type_id, 0.0)) + ) var anchors: PackedVector3Array = _spawn_anchor_positions.get( entry.type_id, PackedVector3Array(), @@ -1399,6 +1443,25 @@ func _target_population(entry: GatherableDataType) -> int: return entry.target_population_for_anchor_count(anchors.size()) +func _resolved_surface_height_range(entry: GatherableDataType) -> Vector2: + var minimum_y := entry.minimum_surface_y + var maximum_y := entry.maximum_surface_y + var saltwater_height := _world.get_saltwater_surface_height() + if is_finite(saltwater_height): + match entry.surface_water_relation: + GatherableDataType.SurfaceWaterRelation.ABOVE_SALT_WATER: + minimum_y = maxf( + minimum_y, + saltwater_height + entry.waterline_margin, + ) + GatherableDataType.SurfaceWaterRelation.BELOW_SALT_WATER: + maximum_y = minf( + maximum_y, + saltwater_height - entry.waterline_margin, + ) + return Vector2(minimum_y, maximum_y) + + func _population_for_type(type_id: StringName) -> int: var count: int = 0 for state: Dictionary in _entities.values(): @@ -1430,6 +1493,7 @@ func _clear_world() -> void: _surface_triangles.clear() _surface_areas.clear() _surface_total_areas.clear() + _surface_height_ranges.clear() _spawn_anchor_positions.clear() _respawns.clear() _next_respawn_by_type.clear() diff --git a/network/player_data_root.gd b/network/player_data_root.gd index 9416db3..e173577 100644 --- a/network/player_data_root.gd +++ b/network/player_data_root.gd @@ -9,12 +9,42 @@ 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 := "netfishing_data.json" +const MANIFEST_FILENAME := "straywild_data.json" +const LEGACY_MANIFEST_FILENAME := "netfishing_data.json" const README_FILENAME := "README.txt" -const ENVIRONMENT_VARIABLE := "NETFISHING_DATA_DIR" -const CREATE_ENVIRONMENT_VARIABLE := "NETFISHING_CREATE_DATA_DIR" -const APPLICATION_ID := "netfishing" +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" @@ -37,23 +67,29 @@ 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) - if OS.has_environment(ENVIRONMENT_VARIABLE): + 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) + var environment_path: String = OS.get_environment(environment_variable) if not environment_path.is_absolute_path(): - return _fail("NETFISHING_DATA_DIR must be an absolute path.") + return _fail("straywild_DATA_DIR must be an absolute path.") if ( - OS.get_environment(CREATE_ENVIRONMENT_VARIABLE) == "1" - and not FileAccess.file_exists( - environment_path.path_join(MANIFEST_FILENAME) + _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)): @@ -80,11 +116,35 @@ func resolve() -> bool: 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("NETFISHING")) + var candidate: String = _normalize(documents.path_join("straywild")) if not candidate.is_absolute_path(): return "" var classification: StringName = classify_candidate_path( @@ -105,20 +165,22 @@ func select_new_root(path: String, app_data: bool = false) -> bool: normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH) if not _validate_candidate(normalized, true): return false - var manifest_path: String = normalized.path_join(MANIFEST_FILENAME) - if FileAccess.file_exists(manifest_path): + 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 NETfishing 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 NETfishing data folder." + "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 NETfishing data folder could not be created.") + 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 @@ -145,14 +207,14 @@ 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 = normalized.path_join(MANIFEST_FILENAME) - if FileAccess.file_exists(manifest_path): + var manifest_path: String = _existing_manifest_path(normalized) + if not manifest_path.is_empty(): var manifest: Dictionary = _read_json(manifest_path) - return ( - {"ok": true, "root_id": str(manifest.get("root_id", ""))} - if _valid_manifest(manifest) - else {"ok": false, "message": "The app-data manifest is malformed."} - ) + 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."} @@ -236,17 +298,19 @@ func _activate_existing(path: String, expected_id: String, permit_creation: bool var normalized: String = _normalize(path) if not _validate_candidate(normalized, permit_creation): return false - var manifest_path: String = normalized.path_join(MANIFEST_FILENAME) - if not FileAccess.file_exists(manifest_path): - return _fail("The selected folder is not a NETfishing data folder.") + 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 NETfishing data-folder manifest is malformed.") + 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 NETfishing data folder is unavailable or unwritable.") + return _fail("The straywild data folder is unavailable or unwritable.") root_path = normalized root_id = found_id requires_selection = false @@ -339,7 +403,7 @@ static func _is_same_or_child_path(candidate: String, reference: String) -> bool func _test_writable(path: String) -> bool: var probe: String = path.path_join( - ".netfishing-write-%s.tmp" % device_id.left(12) + ".straywild-write-%s.tmp" % device_id.left(12) ) var file: FileAccess = FileAccess.open(probe, FileAccess.WRITE) if file == null: @@ -370,7 +434,7 @@ func _create_layout(path: String, id: String) -> bool: ): return false var readme: String = ( - "NETfishing player data\n\n" + "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" @@ -422,12 +486,71 @@ func _valid_manifest(data: Dictionary) -> bool: return ( data.get("format_version") == MANIFEST_VERSION and data.get("layout_version") == LAYOUT_VERSION - and data.get("application") == APPLICATION_ID + 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: diff --git a/network/player_relationship_store.gd b/network/player_relationship_store.gd index 2452315..de04d49 100644 --- a/network/player_relationship_store.gd +++ b/network/player_relationship_store.gd @@ -7,8 +7,8 @@ const FORMAT_VERSION := 2 const MAX_RECORDS := 500 const MAX_FRIENDS := 200 const SOCIAL_TOKEN_LENGTH := 64 -const PRESENCE_CHANNEL_DOMAIN := "NETFISHING_PRESENCE_V1:" -const INVITE_INBOX_DOMAIN := "NETFISHING_INVITE_V1:" +const PRESENCE_CHANNEL_DOMAIN := "straywild_PRESENCE_V1:" +const INVITE_INBOX_DOMAIN := "straywild_INVITE_V1:" var _records: Dictionary = {} var _loaded := false @@ -21,6 +21,10 @@ 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: diff --git a/network/portable_data_migration.gd b/network/portable_data_migration.gd index 17ded9f..2de0acb 100644 --- a/network/portable_data_migration.gd +++ b/network/portable_data_migration.gd @@ -26,19 +26,16 @@ static func migrate_legacy_to( ) -> Dictionary: var normalized: String = destination.simplify_path().trim_suffix("/") if DirAccess.dir_exists_absolute(normalized): - var existing_manifest: String = normalized.path_join( - PlayerDataRoot.MANIFEST_FILENAME - ) - if FileAccess.file_exists(existing_manifest): + if data_root.has_data_manifest(normalized): return { "ok": false, "requires_existing_root_decision": true, - "message": "The selected folder already contains NETfishing data.", + "message": "The selected folder already contains straywild data.", } if not _directory_empty(normalized): return { "ok": false, - "message": "Choose an empty folder or create a NETfishing subfolder.", + "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() @@ -106,16 +103,16 @@ static func migrate_active_to( "message": "This data folder is already active.", } if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized): - if FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)): + if data_root.has_data_manifest(normalized): return { "ok": false, "requires_existing_root_decision": true, "message": ( - "The selected folder already contains NETfishing data. " + "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 NETfishing subfolder."} + 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() ] @@ -207,9 +204,8 @@ static func replace_existing_with_legacy( destination: String, ) -> Dictionary: var normalized: String = destination.simplify_path().trim_suffix("/") - var manifest: String = normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME) - if not FileAccess.file_exists(manifest): - return {"ok": false, "message": "The selected folder is not a NETfishing data root."} + 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( @@ -229,8 +225,8 @@ static func replace_existing_with_active( destination: String, ) -> Dictionary: var normalized: String = destination.simplify_path().trim_suffix("/") - if not FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)): - return {"ok": false, "message": "The selected folder is not a NETfishing data root."} + 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( diff --git a/network/saved_server_store.gd b/network/saved_server_store.gd index de48c2c..4e00259 100644 --- a/network/saved_server_store.gd +++ b/network/saved_server_store.gd @@ -22,6 +22,12 @@ 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: diff --git a/network/server_trust_store.gd b/network/server_trust_store.gd index fda774b..1d4dd29 100644 --- a/network/server_trust_store.gd +++ b/network/server_trust_store.gd @@ -19,6 +19,10 @@ 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: diff --git a/player/net_attachment.gd b/player/net_attachment.gd index 8938a13..3c7843a 100644 --- a/player/net_attachment.gd +++ b/player/net_attachment.gd @@ -1,7 +1,88 @@ extends "res://player/tool_socket_attachment.gd" -@onready var _impact_marker: Marker3D = %ImpactMarker +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: - return _impact_marker.global_position + 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.tscn b/player/net_attachment.tscn index 246e71c..da49a88 100644 --- a/player/net_attachment.tscn +++ b/player/net_attachment.tscn @@ -1,42 +1,7 @@ -[gd_scene load_steps=8 format=3] +[gd_scene load_steps=3 format=3] [ext_resource type="Script" path="res://player/net_attachment.gd" id="1_script"] - -[sub_resource type="StandardMaterial3D" id="HandleMaterial"] -albedo_color = Color(0.25, 0.13, 0.055, 1) -roughness = 0.9 - -[sub_resource type="CylinderMesh" id="HandleMesh"] -material = SubResource("HandleMaterial") -top_radius = 0.025 -bottom_radius = 0.035 -height = 1.25 - -[sub_resource type="StandardMaterial3D" id="HoopMaterial"] -albedo_color = Color(0.34, 0.39, 0.4, 1) -metallic = 0.25 -roughness = 0.65 - -[sub_resource type="TorusMesh" id="HoopMesh"] -material = SubResource("HoopMaterial") -inner_radius = 0.24 -outer_radius = 0.29 -rings = 12 -ring_segments = 18 - -[sub_resource type="StandardMaterial3D" id="NetMaterial"] -transparency = 1 -albedo_color = Color(0.65, 0.78, 0.75, 0.42) -shading_mode = 0 -cull_mode = 2 - -[sub_resource type="CylinderMesh" id="NetMesh"] -material = SubResource("NetMaterial") -top_radius = 0.24 -bottom_radius = 0.08 -height = 0.34 -radial_segments = 12 -rings = 1 +[ext_resource type="PackedScene" path="res://art/exported/items/equipment/net.glb" id="2_model"] [node name="NetAttachment" type="BoneAttachment3D"] bone_name = "rod_socket" @@ -47,18 +12,9 @@ aligned_mount_path = NodePath("CatchingNet") unique_name_in_owner = true visible = false -[node name="Handle" type="MeshInstance3D" parent="CatchingNet"] -position = Vector3(0, 0.55, 0) -mesh = SubResource("HandleMesh") +[node name="ModelMount" type="Node3D" parent="CatchingNet"] +position = Vector3(-0.000217, -0.098636, -0.020218) +rotation = Vector3(0, 3.1415927, 0) -[node name="Hoop" type="MeshInstance3D" parent="CatchingNet"] -position = Vector3(0, 1.17, 0) -mesh = SubResource("HoopMesh") - -[node name="Bag" type="MeshInstance3D" parent="CatchingNet"] -position = Vector3(0, 1, 0) -mesh = SubResource("NetMesh") - -[node name="ImpactMarker" type="Marker3D" parent="CatchingNet"] +[node name="NetModel" parent="CatchingNet/ModelMount" unique_id=1627767240 instance=ExtResource("2_model")] unique_name_in_owner = true -position = Vector3(0, 1.17, 0) diff --git a/player/player.gd b/player/player.gd index 5ae4cac..78dac3d 100644 --- a/player/player.gd +++ b/player/player.gd @@ -90,6 +90,7 @@ const CATCH_SHOWCASE_INPUT_OWNER: StringName = &"catch_showcase" # 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_WALKING_ANIMATION, CHARACTER_CASTING_ANIMATION, CHARACTER_NET_DRAW_ANIMATION, CHARACTER_NET_STRIKE_ANIMATION, @@ -427,12 +428,20 @@ class ShowcaseCameraSnapshot: @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.0, 1.0, 0.01) var fishing_reel_zoom_amount: float = 0.22 +@export_range(0.0, 1.5, 0.01) var fishing_barrier_break_zoom: float = 0.38 +@export_range(0.05, 1.0, 0.01) var fishing_break_zoom_decay: float = 0.34 +@export_range(0.0, 0.2, 0.005) var fishing_barrier_hit_shake: float = 0.018 +@export_range(0.0, 0.3, 0.005) var fishing_barrier_break_shake: float = 0.045 @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 @@ -471,6 +480,8 @@ 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) var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) @@ -492,6 +503,12 @@ 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 _fishing_reel_camera_active: bool = false +var _fishing_break_zoom_offset: float = 0.0 +var _fishing_camera_shake_remaining: float = 0.0 +var _fishing_camera_shake_duration: float = 0.0 +var _fishing_camera_shake_strength: float = 0.0 +var _fishing_camera_shake_phase: float = 0.0 var _showcase_rod_visibility: bool = true var _showcase_net_visibility: bool = false var _showcase_shovel_visibility: bool = false @@ -992,6 +1009,7 @@ func _initialize_held_item_attachment() -> void: _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 @@ -1036,6 +1054,8 @@ func _simulate_movement_physics(delta: float) -> void: 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. @@ -1154,6 +1174,7 @@ func _process(delta: float) -> void: ) if not local_control_enabled: return + _update_fishing_camera_effects(delta) if _free_camera_active: if _is_camera_input_enabled(): _rotate_free_camera( @@ -1182,13 +1203,68 @@ func _process(delta: float) -> void: ) var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta) + var fishing_zoom_offset: float = ( + fishing_reel_zoom_amount + if _fishing_reel_camera_active + else 0.0 + ) + _fishing_break_zoom_offset + var effective_target_zoom: float = clampf( + _target_zoom - fishing_zoom_offset, + 1.25 if _rv_interior_camera_mode else minimum_zoom, + 16.0 if _rv_interior_camera_mode else maximum_zoom, + ) _spring_arm.spring_length = lerpf( _spring_arm.spring_length, - _target_zoom, + effective_target_zoom, zoom_weight ) +func set_fishing_reel_camera_active(active: bool) -> void: + _fishing_reel_camera_active = active + + +func play_fishing_barrier_camera_feedback(barrier_broken: bool) -> void: + if not local_control_enabled or _free_camera_active: + return + _fishing_camera_shake_strength = ( + fishing_barrier_break_shake + if barrier_broken + else fishing_barrier_hit_shake + ) + _fishing_camera_shake_duration = 0.20 if barrier_broken else 0.10 + _fishing_camera_shake_remaining = _fishing_camera_shake_duration + if barrier_broken: + _fishing_break_zoom_offset = fishing_barrier_break_zoom + + +func _update_fishing_camera_effects(delta: float) -> void: + _fishing_break_zoom_offset = move_toward( + _fishing_break_zoom_offset, + 0.0, + ( + fishing_barrier_break_zoom + / maxf(fishing_break_zoom_decay, 0.05) + ) * delta, + ) + if _fishing_camera_shake_remaining <= 0.0: + _camera.h_offset = 0.0 + _camera.v_offset = 0.0 + return + _fishing_camera_shake_remaining = maxf( + _fishing_camera_shake_remaining - delta, + 0.0, + ) + _fishing_camera_shake_phase += delta * 92.0 + var decay: float = ( + _fishing_camera_shake_remaining + / maxf(_fishing_camera_shake_duration, 0.001) + ) + var strength: float = _fishing_camera_shake_strength * decay + _camera.h_offset = sin(_fishing_camera_shake_phase) * strength + _camera.v_offset = cos(_fishing_camera_shake_phase * 1.31) * strength * 0.7 + + func _update_blink(delta: float) -> void: if _fighting_visual_active: return @@ -1320,9 +1396,7 @@ func _update_character_animation() -> void: and supports_network_animation_action(animation_action_id) and _character_animation_player.has_animation(animation_action_id) ) - var held_show_item_visible: bool = ( - _held_fish_visible or _held_art_kit_visible - ) + var held_show_item_visible: bool = _has_held_show_item() var requested_animation: Array[StringName] = [] if _pocket_visual_active: requested_animation = [_pocket_visual_animation] @@ -1635,6 +1709,7 @@ func _complete_retract_animation() -> void: 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: @@ -1667,12 +1742,12 @@ func _set_sitting( should_sit: bool, is_local_intent: bool = false, ) -> void: - if _sitting == should_sit: - return - _sitting = should_sit 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 = &"" @@ -1687,11 +1762,23 @@ 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 @@ -1751,7 +1838,11 @@ func _unhandled_input(event: InputEvent) -> void: var mouse_zoom_in: bool = ( event is InputEventMouseButton - and event.shift_pressed != _swap_hotbar_camera_scroll + 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_in") ) var controller_zoom_in: bool = ( @@ -1760,7 +1851,11 @@ func _unhandled_input(event: InputEvent) -> void: ) var mouse_zoom_out: bool = ( event is InputEventMouseButton - and event.shift_pressed != _swap_hotbar_camera_scroll + 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 = ( @@ -1948,7 +2043,11 @@ func get_active_gameplay_camera() -> Camera3D: func _set_target_zoom(value: float) -> void: - _target_zoom = clampf(value, minimum_zoom, maximum_zoom) + _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: @@ -2062,6 +2161,37 @@ func reset_network_movement_state() -> void: _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 @@ -2352,6 +2482,21 @@ func apply_local_prediction_correction( _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() @@ -2386,22 +2531,34 @@ func apply_local_prediction_correction( correction.normalized() * LOCAL_PREDICTION_MAX_SOFT_CORRECTION_STEP ) - _apply_camera_safe_local_correction(correction) - _local_prediction_soft_corrections += 1 + if _apply_camera_safe_local_correction(correction): + _local_prediction_soft_corrections += 1 -func _apply_camera_safe_local_correction(correction: Vector3) -> void: +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 + 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 ) - global_position += correction + # 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 = ( @@ -2410,6 +2567,7 @@ func _apply_camera_safe_local_correction(correction: Vector3) -> void: _local_reconciliation_camera_offset = ( _camera_yaw.position - base_camera_local_position ) + return true func _reset_local_prediction_error() -> void: @@ -2625,6 +2783,9 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary: 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() @@ -2636,6 +2797,7 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary: "velocity": parsed_velocity, "visual_yaw": visual_yaw, "animation_state": animation_state, + "grounded": grounded, "acknowledged_input": acknowledged_input, "sitting": sitting, "casting": casting, @@ -2958,6 +3120,19 @@ func set_camera_active(active: bool) -> void: _camera.current = active +func face_world_direction(direction: Vector3) -> void: + var horizontal_direction := Vector3(direction.x, 0.0, direction.z) + if horizontal_direction.is_zero_approx() or _visuals == null: + return + horizontal_direction = horizontal_direction.normalized() + var target_world_yaw: float = atan2( + -horizontal_direction.x, + -horizontal_direction.z, + ) + _visuals.rotation.y = target_world_yaw - global_rotation.y + _network_target_visual_yaw = _visuals.rotation.y + + func apply_camera_settings( new_mouse_sensitivity: float, new_controller_sensitivity: float, @@ -2974,6 +3149,49 @@ func apply_camera_settings( _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: return _camera_input_enabled @@ -3100,6 +3318,23 @@ func set_active_shovel(should_show: bool) -> void: ) +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: @@ -3272,7 +3507,52 @@ func set_held_fish( func _has_held_show_item() -> bool: - return _held_fish_visible or _held_art_kit_visible + 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: diff --git a/player/player.tscn b/player/player.tscn index 4a2d897..3288d2d 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -12,7 +12,7 @@ [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/netfishing_base_character.glb" id="13_character"] +[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"] diff --git a/player/player_visual_presenter.gd b/player/player_visual_presenter.gd index 745fc3c..b071b2c 100644 --- a/player/player_visual_presenter.gd +++ b/player/player_visual_presenter.gd @@ -3,9 +3,9 @@ extends RefCounted const PlayerScene: PackedScene = preload("res://player/player.tscn") const FurPatternShader: Shader = preload("res://player/fur_pattern.gdshader") -const RUNTIME_MATERIAL_META: StringName = &"netfishing_runtime_material" +const RUNTIME_MATERIAL_META: StringName = &"straywild_runtime_material" const RUNTIME_FUR_PATTERN_MATERIAL_META: StringName = ( - &"netfishing_runtime_fur_pattern_material" + &"straywild_runtime_fur_pattern_material" ) const EAR_MESH_NAMES: Array[String] = [ "ears_antlers_round", diff --git a/progression/player_appearance_store.gd b/progression/player_appearance_store.gd index 7ac8767..febe43d 100644 --- a/progression/player_appearance_store.gd +++ b/progression/player_appearance_store.gd @@ -13,6 +13,10 @@ 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: diff --git a/progression/player_cooler_capacity.gd b/progression/player_cooler_capacity.gd index b8c62be..bbd0b87 100644 --- a/progression/player_cooler_capacity.gd +++ b/progression/player_cooler_capacity.gd @@ -2,62 +2,77 @@ class_name PlayerCoolerCapacity extends Node const PlayerWalletType = preload("res://economy/player_wallet.gd") +const PlayerHomeStateType = preload("res://homes/player_home_state.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. +## Compatibility adapter for saves and systems that still refer to the old +## cooler-capacity object. Storage is now derived exclusively from the player's +## home tier; this object is not independently purchasable. 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 -var _capacity_level: int = 0 +var _home_state: PlayerHomeStateType +var _legacy_capacity_level: int = 0 + + +func bind_home_state(home_state: PlayerHomeStateType) -> void: + if _home_state == home_state: + return + if ( + _home_state != null + and _home_state.changed.is_connected(_on_home_changed) + ): + _home_state.changed.disconnect(_on_home_changed) + _home_state = home_state + if ( + _home_state != null + and not _home_state.changed.is_connected(_on_home_changed) + ): + _home_state.changed.connect(_on_home_changed) + _on_home_changed() func get_level() -> int: - return _capacity_level + return _legacy_capacity_level func get_capacity() -> int: - return CAPACITIES[_capacity_level] + if _home_state != null: + # A new home upgrade grants its authored capacity. Older saves retain any + # separately purchased storage level so reducing the first RV tier cannot + # hide or discard items that were already placed in later slots. + return maxi( + _home_state.get_storage_capacity(), + CAPACITIES[clampi(_legacy_capacity_level, 0, MAX_LEVEL)], + ) + return CAPACITIES[clampi(_legacy_capacity_level, 0, MAX_LEVEL)] func get_next_capacity() -> int: - if _capacity_level >= MAX_LEVEL: - return -1 - return CAPACITIES[_capacity_level + 1] + return -1 func get_next_cost() -> int: - if _capacity_level >= MAX_LEVEL: - return -1 - return EXPANSION_COSTS[_capacity_level] + return -1 -func can_purchase(wallet: PlayerWalletType) -> bool: - var cost: int = get_next_cost() - return wallet != null and cost >= 0 and wallet.can_afford(cost) +func can_purchase(_wallet: PlayerWalletType) -> bool: + return false -func purchase(wallet: PlayerWalletType) -> bool: - if not can_purchase(wallet): - return false - var cost: int = get_next_cost() - if not wallet.debit(cost): - return false - _capacity_level += 1 - capacity_changed.emit(_capacity_level, get_capacity()) - return true +func purchase(_wallet: PlayerWalletType) -> bool: + return false func restore_level(level: int) -> bool: var validated: int = clampi(level, 0, MAX_LEVEL) - var changed: bool = _capacity_level != validated - _capacity_level = validated + var changed: bool = _legacy_capacity_level != validated + _legacy_capacity_level = validated if changed: - capacity_changed.emit(_capacity_level, get_capacity()) + capacity_changed.emit(get_level(), get_capacity()) return true @@ -66,4 +81,8 @@ func reset_to_defaults() -> void: func to_save_data() -> Dictionary: - return {"capacity_level": _capacity_level} + return {"capacity_level": get_level()} + + +func _on_home_changed() -> void: + capacity_changed.emit(get_level(), get_capacity()) diff --git a/project.godot b/project.godot index 89bf703..5bcbb4d 100644 --- a/project.godot +++ b/project.godot @@ -10,11 +10,11 @@ config_version=5 [application] -config/name="NETFISHING" -config/version="0.18.0-alpha" +config/name="straywild" +config/version="0.20.2-alpha" run/main_scene="res://main/main.tscn" config/features=PackedStringArray("4.7", "GL Compatibility") -config/icon="res://art/exported/system_icons/netfishing_256.png" +config/icon="res://art/exported/system_icons/straywild_256.png" [autoload] @@ -26,7 +26,7 @@ window/stretch/aspect="expand" [editor_plugins] -enabled=PackedStringArray("res://addons/netfishing_shoreline_baker/plugin.cfg") +enabled=PackedStringArray("res://addons/straywild_shoreline_baker/plugin.cfg") [gui] @@ -260,7 +260,7 @@ alternate_fishing={ [network] -discovery/base_url="https://discovery.netfishing.org" +discovery/base_url="https://discovery.straywild.io/patron-test" [physics] diff --git a/save/player_save_manager.gd b/save/player_save_manager.gd index 1ad74a1..c41f861 100644 --- a/save/player_save_manager.gd +++ b/save/player_save_manager.gd @@ -34,8 +34,9 @@ const WorldWeatherServiceType = preload( 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 = 10 +const SAVE_VERSION: int = 11 const LEGACY_SAVE_FILENAME := "player_save.json" const ARCHIVE_EXTENSION := ".nfsave" const BASIC_ROD_ID: StringName = &"basic_fishing_rod" @@ -76,6 +77,7 @@ class LoadSnapshot: 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 @@ -96,6 +98,7 @@ 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 @@ -175,6 +178,7 @@ func setup( world_weather: WorldWeatherServiceType, jobs: PlayerJobServiceType, player: PlayerType, + home_state: PlayerHomeStateType, ) -> void: _inventory = inventory _collection_log = collection_log @@ -192,6 +196,7 @@ func setup( _world_weather = world_weather _jobs = jobs _player = player + _home_state = home_state _is_configured = ( _inventory != null and _collection_log != null @@ -209,6 +214,7 @@ func setup( 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.") @@ -255,6 +261,8 @@ func setup( _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: @@ -330,6 +338,11 @@ func load_player_data() -> bool: snapshot.reel_speed_level, snapshot.barrier_power_level ) + # Home tier is now the authority for private-storage capacity, so restore it + # before the compatibility capacity adapter and inventory layout. + var home_restored: bool = _home_state.restore_from_save_data( + snapshot.home_data + ) var cooler_restored: bool = _cooler_capacity.restore_level( snapshot.cooler_capacity_level ) @@ -376,13 +389,14 @@ func load_player_data() -> bool: 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" + + "time=%s weather=%s jobs=%s home=%s" ) % [ inventory_restored, @@ -399,6 +413,7 @@ func load_player_data() -> bool: world_time_restored, world_weather_restored, jobs_restored, + home_restored, ] ) return false @@ -899,6 +914,7 @@ func _build_save_dictionary() -> Dictionary: ), }, "jobs": _jobs.to_save_data(), + "home": _home_state.to_save_data(), } @@ -913,6 +929,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: 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"] @@ -924,6 +941,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: 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"] @@ -1006,6 +1024,12 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: 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"] @@ -1302,6 +1326,26 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: PlayerCoolerCapacityType.MAX_LEVEL, "cooler.capacity_level" ) + # Before homes had upgrade tiers, storage was purchased separately. Preserve + # those saves by granting the starter RV to anybody who owned at least one + # legacy expansion. PlayerCoolerCapacity also retains the purchased legacy + # capacity so entries beyond the new RV's first 18 slots remain accessible. + if ( + snapshot.cooler_capacity_level > 0 + and int(snapshot.home_data.get("tier", 0)) + == PlayerHomeStateType.STARTER_TIER + ): + var migrated_home: Dictionary = home_data.duplicate(true) + migrated_home["tier"] = PlayerHomeStateType.STARTER_RV_TIER + migrated_home["exterior_id"] = String( + PlayerHomeStateType.STARTER_RV_EXTERIOR_ID + ) + var sanitized_migrated_home := ( + PlayerHomeStateType.sanitize_save_data(migrated_home) + ) + if sanitized_migrated_home.is_empty(): + return null + snapshot.home_data = sanitized_migrated_home snapshot.art_unlock_mask = _read_integer( art_data.get("unlock_mask"), 0, @@ -1340,6 +1384,8 @@ func _migrate_save( 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(): @@ -1597,6 +1643,13 @@ func _migrate_version_9_to_10(data: Dictionary) -> Dictionary: 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 @@ -1866,6 +1919,7 @@ func _restore_defaults() -> void: _world_layout = WorldLayoutType.GENERATED _world_seed = DEFAULT_WORLD_SEED _jobs.reset_to_defaults() + _home_state.reset_to_defaults() _is_restoring = false _is_dirty = false diff --git a/save/progression_save_codec.gd b/save/progression_save_codec.gd index 7844e16..4d0ba33 100644 --- a/save/progression_save_codec.gd +++ b/save/progression_save_codec.gd @@ -1,7 +1,8 @@ class_name ProgressionSaveCodec extends RefCounted -const MAGIC := "NETFISHING_PROGRESSION_CONTAINER" +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" @@ -9,6 +10,8 @@ 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" ) @@ -102,15 +105,9 @@ static func _read_container(path: String, expected_kind: String) -> Dictionary: or FileAccess.get_size(path) > MAX_CONTAINER_BYTES ): return {"ok": false, "message": "progression file is unavailable."} - var file: FileAccess = FileAccess.open_encrypted_with_pass( - path, - FileAccess.READ, - CONTAINER_PASSPHRASE, - ) - if file == null: + 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_text: String = file.get_as_text() - file.close() var envelope_json := JSON.new() if ( envelope_json.parse(envelope_text) != OK @@ -118,8 +115,9 @@ static func _read_container(path: String, expected_kind: String) -> 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") != MAGIC + envelope.get("magic") not in [MAGIC, LEGACY_MAGIC] or int(envelope.get("format_version", -1)) != FORMAT_VERSION or str(envelope.get("kind", "")) != expected_kind ): @@ -144,9 +142,23 @@ static func _read_container(path: String, expected_kind: String) -> Dictionary: "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, diff --git a/scripts/build_android_debug.sh b/scripts/build_android_debug.sh index 8f856f8..f68eb32 100755 --- a/scripts/build_android_debug.sh +++ b/scripts/build_android_debug.sh @@ -4,7 +4,7 @@ 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/NETfishing-debug.apk" +readonly OUTPUT_PATH="${PROJECT_ROOT}/builds/android/straywild-debug.apk" readonly GODOT_BIN="${GODOT_BIN:-godot}" mkdir -p -- "$(dirname -- "${OUTPUT_PATH}")" diff --git a/scripts/build_playtest.sh b/scripts/build_playtest.sh index fb00aec..d54ba70 100755 --- a/scripts/build_playtest.sh +++ b/scripts/build_playtest.sh @@ -4,14 +4,14 @@ 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.18.0-alpha" +readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.20.2-alpha" 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/netfishing" -readonly WINDOWS_ZIP="${BUILD_ROOT}/NETfishing-v0.18.0-alpha-windows-x86_64.zip" -readonly LINUX_ZIP="${BUILD_ROOT}/NETfishing-v0.18.0-alpha-linux-x86_64.zip" +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 GODOT_BIN="${GODOT_BIN:-godot}" if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then @@ -31,26 +31,26 @@ mkdir -p -- "${WINDOWS_DIR}" "${LINUX_DIR}" --headless \ --path "${PROJECT_ROOT}" \ --export-release "Windows Desktop" \ - "${WINDOWS_DIR}/NETfishing.exe" + "${WINDOWS_DIR}/straywild.exe" "${GODOT_BIN}" \ --headless \ --path "${PROJECT_ROOT}" \ --export-release "Linux Desktop" \ - "${LINUX_DIR}/NETfishing.x86_64" + "${LINUX_DIR}/straywild.x86_64" for required_file in \ - "${WINDOWS_DIR}/NETfishing.exe" \ - "${WINDOWS_DIR}/NETfishing.pck" \ - "${LINUX_DIR}/NETfishing.x86_64" \ - "${LINUX_DIR}/NETfishing.pck"; do + "${WINDOWS_DIR}/straywild.exe" \ + "${WINDOWS_DIR}/straywild.pck" \ + "${LINUX_DIR}/straywild.x86_64" \ + "${LINUX_DIR}/straywild.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}/NETfishing.x86_64" +chmod +x -- "${LINUX_DIR}/straywild.x86_64" cp -- "${README_SOURCE}" "${WINDOWS_DIR}/README-PLAYTEST.txt" cp -- "${README_SOURCE}" "${LINUX_DIR}/README-PLAYTEST.txt" @@ -60,7 +60,7 @@ stage_licenses() { install -m 0644 "${PROJECT_ROOT}/LICENSE" \ "${destination}/GPL-3.0-or-later.txt" install -m 0644 "${PROJECT_ROOT}/ASSET-LICENSE.md" \ - "${destination}/NETFISHING-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" \ @@ -72,11 +72,14 @@ stage_licenses() { cat >"${destination}/SOURCE-CODE.md" <&2 exit 1 fi -if [[ "${NETFISHING_ALLOW_DIRTY_RELEASE:-0}" != "1" ]]; then +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 @@ -97,7 +97,7 @@ done mkdir -p -- "${ARM64_ROOT}" "${RELEASE_ROOT}" if [[ ${PACKAGE_ONLY} -eq 0 ]]; then readonly EXPORT_PROJECT_ROOT="$( - mktemp -d -t netfishing-portmaster-export.XXXXXX + mktemp -d -t straywild-portmaster-export.XXXXXX )" cleanup_export_project() { rm -rf -- "${EXPORT_PROJECT_ROOT}" @@ -108,9 +108,9 @@ if [[ ${PACKAGE_ONLY} -eq 0 ]]; then --exclude '/.git/' \ --exclude '/.godot/' \ --exclude '/art/source/' \ - --exclude '/art/exported/system_icons/netfishing.ico' \ - --exclude '/art/exported/system_icons/netfishing_1024.png' \ - --exclude '/art/exported/system_icons/netfishing_1024.png.import' \ + --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/' \ @@ -141,14 +141,14 @@ rm -f -- \ "${OBSOLETE_LOWERCASE_ARCHIVE}" mkdir -p -- "${GAME_ROOT}/licenses" -install -m 0755 "${TEMPLATE_ROOT}/NETfishing.sh" "${STAGE_ROOT}/NETfishing.sh" +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}/netfishing.gptk" \ - "${GAME_ROOT}/netfishing.gptk" -install -m 0755 "${TEMPLATE_ROOT}/launch-netfishing.sh" \ - "${GAME_ROOT}/launch-netfishing.sh" -install -m 0755 "${ARM64_EXECUTABLE}" "${GAME_ROOT}/NETfishing.aarch64" -install -m 0644 "${ARM64_PCK}" "${GAME_ROOT}/NETfishing.pck" +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 \ @@ -159,7 +159,7 @@ install -m 0644 \ "${GAME_ROOT}/licenses/GPL-3.0-or-later.txt" install -m 0644 \ "${PROJECT_ROOT}/ASSET-LICENSE.md" \ - "${GAME_ROOT}/licenses/NETFISHING-ASSET-LICENSE.md" + "${GAME_ROOT}/licenses/straywild-ASSET-LICENSE.md" install -m 0644 \ "${PROJECT_ROOT}/TRADEMARKS.md" \ "${GAME_ROOT}/licenses/TRADEMARKS.md" @@ -171,7 +171,7 @@ install -m 0644 \ "${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' NETfishing.sh netfishing)" +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": "netfishing.zip"' "${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 netfishing/, not at the archive root." >&2 + echo "port.json must be inside straywild/, not at the archive root." >&2 exit 1 fi if grep -Eq '^[[:space:]]*#[[:space:]]*PORTMASTER:' \ - "${STAGE_ROOT}/NETfishing.sh"; then + "${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}/NETfishing.sh" -grep -Fq 'NETFISHING_PERFORMANCE_PROFILE=light' \ - "${STAGE_ROOT}/NETfishing.sh" + "${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}/NETfishing.sh" -grep -Fq 'SAVEDIR="$PORTS_ROOT/saves/netfishing"' \ - "${STAGE_ROOT}/NETfishing.sh" -grep -Fq '"NETFISHING_DATA_DIR=$SAVEDIR"' \ - "${STAGE_ROOT}/NETfishing.sh" -grep -Fq '"NETFISHING_CREATE_DATA_DIR=1"' \ - "${STAGE_ROOT}/NETfishing.sh" -grep -Fq 'printf '\''%s\n'\'' "$netfishing_controllerconfig" > "$CONTROLLER_MAPPING_FILE"' \ - "${STAGE_ROOT}/NETfishing.sh" -grep -Fq '"$GAME_LAUNCHER"' "${STAGE_ROOT}/NETfishing.sh" -grep -Fq '"$CONTROLLER_MAPPING_FILE"' "${STAGE_ROOT}/NETfishing.sh" -if grep -Fq 'SDL_GAMECONTROLLERCONFIG="$netfishing_controllerconfig"' \ - "${STAGE_ROOT}/NETfishing.sh"; then + "${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-netfishing.sh" + "${GAME_ROOT}/launch-straywild.sh" grep -Fq 'export SDL_GAMECONTROLLERCONFIG' \ - "${GAME_ROOT}/launch-netfishing.sh" -grep -Fq 'exec "$@"' "${GAME_ROOT}/launch-netfishing.sh" -grep -Fq 'GPTOKEYB_CONFIG="$GAMEDIR/netfishing.gptk"' \ - "${STAGE_ROOT}/NETfishing.sh" -grep -Fq '$GPTOKEYB "NETfishing.aarch64" -c "$GPTOKEYB_CONFIG" &' \ - "${STAGE_ROOT}/NETfishing.sh" + "${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}/NETfishing.sh" -grep -Fq 'pm_finish' "${STAGE_ROOT}/NETfishing.sh" + "${STAGE_ROOT}/straywild.sh" +grep -Fq 'pm_finish' "${STAGE_ROOT}/straywild.sh" if [[ "$(grep -Ec '^[[:space:]]*\$GPTOKEYB[[:space:]]' \ - "${STAGE_ROOT}/NETfishing.sh")" -ne 1 ]]; then - echo "NETfishing must start exactly one GPTOKEYB exit handler." >&2 + "${STAGE_ROOT}/straywild.sh")" -ne 1 ]]; then + echo "straywild must start exactly one GPTOKEYB exit handler." >&2 exit 1 fi readonly GPTOKEYB_NOOP_KEYS=( @@ -303,13 +307,13 @@ readonly GPTOKEYB_NOOP_KEYS=( for noop_key in "${GPTOKEYB_NOOP_KEYS[@]}"; do if ! grep -Eq \ "^[[:space:]]*${noop_key}[[:space:]]*=[[:space:]]*\\\\\"[[:space:]]*$" \ - "${GAME_ROOT}/netfishing.gptk"; then + "${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}/netfishing.gptk")" -ne "${#GPTOKEYB_NOOP_KEYS[@]}" ]]; then + "${GAME_ROOT}/straywild.gptk")" -ne "${#GPTOKEYB_NOOP_KEYS[@]}" ]]; then echo "GPTOKEYB no-op mapping contains an unexpected assignment." >&2 exit 1 fi diff --git a/scripts/import_animalese_voice.sh b/scripts/import_animalese_voice.sh index cb7eded..b9076ca 100755 --- a/scripts/import_animalese_voice.sh +++ b/scripts/import_animalese_voice.sh @@ -35,10 +35,10 @@ if [[ ! "$SAMPLE_SET_DIRECTORY" =~ ^[a-z0-9_-]+$ ]]; then fi readonly DESTINATION_DIRECTORY="$PROJECT_ROOT/sound/dialogue/animalese/$SAMPLE_SET_DIRECTORY" -readonly WORK_DIRECTORY="$(mktemp -d -t netfishing-animalese-import.XXXXXX)" +readonly WORK_DIRECTORY="$(mktemp -d -t straywild-animalese-import.XXXXXX)" cleanup() { - if [[ "$WORK_DIRECTORY" == /tmp/netfishing-animalese-import.* ]]; then + if [[ "$WORK_DIRECTORY" == /tmp/straywild-animalese-import.* ]]; then find "$WORK_DIRECTORY" -depth -delete fi } diff --git a/scripts/normalize_texture_profiles.sh b/scripts/normalize_texture_profiles.sh index 1483a61..0551efd 100755 --- a/scripts/normalize_texture_profiles.sh +++ b/scripts/normalize_texture_profiles.sh @@ -13,7 +13,7 @@ fi readonly ROOT_PATH="$1" -readonly WORK_ROOT="$(mktemp -d -t netfishing-godot-data.XXXXXX)" +readonly WORK_ROOT="$(mktemp -d -t straywild-godot-data.XXXXXX)" trap 'rm -rf -- "${WORK_ROOT}"' EXIT cd "${PROJECT_ROOT}" diff --git a/scripts/portmaster/gameinfo.xml b/scripts/portmaster/gameinfo.xml index ef608bd..1b0a1c7 100755 --- a/scripts/portmaster/gameinfo.xml +++ b/scripts/portmaster/gameinfo.xml @@ -1,13 +1,13 @@ - ./NETfishing.sh - NETfishing + ./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 - NETfishing project + straywild project Independent adventure, simulation - ./netfishing/screenshot.png + ./straywild/screenshot.png diff --git a/scripts/portmaster/launch-netfishing.sh b/scripts/portmaster/launch-straywild.sh similarity index 100% rename from scripts/portmaster/launch-netfishing.sh rename to scripts/portmaster/launch-straywild.sh diff --git a/scripts/portmaster/port.json b/scripts/portmaster/port.json index affc601..4c36648 100644 --- a/scripts/portmaster/port.json +++ b/scripts/portmaster/port.json @@ -1,13 +1,13 @@ { "version": 4, - "name": "netfishing.zip", + "name": "straywild.zip", "items": [ - "NETfishing.sh", - "netfishing/" + "straywild.sh", + "straywild/" ], "items_opt": null, "attr": { - "title": "NETfishing", + "title": "straywild", "porter": [ "Voyager" ], diff --git a/scripts/portmaster/netfishing.gptk b/scripts/portmaster/straywild.gptk similarity index 87% rename from scripts/portmaster/netfishing.gptk rename to scripts/portmaster/straywild.gptk index ff1ad36..2e8602e 100644 --- a/scripts/portmaster/netfishing.gptk +++ b/scripts/portmaster/straywild.gptk @@ -1,4 +1,4 @@ -# NETfishing reads the controller directly. GPTOKEYB is retained only for its +# 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. diff --git a/scripts/portmaster/NETfishing.sh b/scripts/portmaster/straywild.sh similarity index 58% rename from scripts/portmaster/NETfishing.sh rename to scripts/portmaster/straywild.sh index 8e118da..d4e56a7 100755 --- a/scripts/portmaster/NETfishing.sh +++ b/scripts/portmaster/straywild.sh @@ -16,35 +16,56 @@ source "$controlfolder/control.txt" [ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt" get_controls -GAMEDIR="/${directory}/ports/netfishing" +GAMEDIR="/${directory}/ports/straywild" CONFDIR="$GAMEDIR/conf" -PORTS_ROOT="${GAMEDIR%/netfishing}" -SAVEDIR="$PORTS_ROOT/saves/netfishing" -DATA_BOOTSTRAP_DIR="$CONFDIR/data/godot/app_userdata/NETFISHING" -GAME_EXECUTABLE="$GAMEDIR/NETfishing.aarch64" -GAME_LAUNCHER="$GAMEDIR/launch-netfishing.sh" -GPTOKEYB_CONFIG="$GAMEDIR/netfishing.gptk" +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/netfishing-weston" +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" -# NETfishing uses a predictable save directory beside the port instead of +# 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. -NETFISHING_DATA_ENVIRONMENT=() -if [ -f "$SAVEDIR/netfishing_data.json" ] || { +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 "$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" - NETFISHING_DATA_ENVIRONMENT+=( - "NETFISHING_DATA_DIR=$SAVEDIR" - "NETFISHING_CREATE_DATA_DIR=1" + straywild_DATA_ENVIRONMENT+=( + "straywild_DATA_DIR=$SAVEDIR" + "straywild_CREATE_DATA_DIR=1" ) fi @@ -52,7 +73,7 @@ fi if [ ! -f "$controlfolder/libs/${WESTON_RUNTIME}.squashfs" ]; then if [ ! -x "$HARBOURMASTER" ]; then - pm_message "NETfishing requires the latest PortMaster and WestonPack runtime." + pm_message "straywild requires the latest PortMaster and WestonPack runtime." sleep 5 exit 1 fi @@ -78,17 +99,17 @@ cd "$GAMEDIR" || exit 1 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 - netfishing_controllerconfig="$GODOT_MUOS_MAPPING" + straywild_controllerconfig="$GODOT_MUOS_MAPPING" elif [ -n "$sdl_controllerconfig" ]; then - netfishing_controllerconfig="$sdl_controllerconfig" + straywild_controllerconfig="$sdl_controllerconfig" else - netfishing_controllerconfig="$GODOT_MUOS_MAPPING" + straywild_controllerconfig="$GODOT_MUOS_MAPPING" fi -printf '%s\n' "$netfishing_controllerconfig" > "$CONTROLLER_MAPPING_FILE" +printf '%s\n' "$straywild_controllerconfig" > "$CONTROLLER_MAPPING_FILE" -NETFISHING_GODOT_OPTIONS=() -NETFISHING_GAME_ENVIRONMENT=() -PERFORMANCE_PROFILE="${NETFISHING_PERFORMANCE_PROFILE:-}" +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 @@ -96,16 +117,16 @@ if [[ -z "$PERFORMANCE_PROFILE" && -r "$PROFILE_PATH" ]]; then fi case "$PERFORMANCE_PROFILE" in normal) - NETFISHING_GAME_ENVIRONMENT+=( - "NETFISHING_PERFORMANCE_PROFILE=normal" + straywild_GAME_ENVIRONMENT+=( + "straywild_PERFORMANCE_PROFILE=normal" ) ;; light|"") - NETFISHING_GAME_ENVIRONMENT+=( - "NETFISHING_PERFORMANCE_PROFILE=light" - "NETFISHING_LOW_END=1" + straywild_GAME_ENVIRONMENT+=( + "straywild_PERFORMANCE_PROFILE=light" + "straywild_LOW_END=1" ) - NETFISHING_GODOT_OPTIONS+=( + straywild_GODOT_OPTIONS+=( --single-window --disable-vsync --max-fps 30 @@ -114,11 +135,11 @@ case "$PERFORMANCE_PROFILE" in ;; *) echo "Unknown performance profile '$PERFORMANCE_PROFILE'; using light." >&2 - NETFISHING_GAME_ENVIRONMENT+=( - "NETFISHING_PERFORMANCE_PROFILE=light" - "NETFISHING_LOW_END=1" + straywild_GAME_ENVIRONMENT+=( + "straywild_PERFORMANCE_PROFILE=light" + "straywild_LOW_END=1" ) - NETFISHING_GODOT_OPTIONS+=( + straywild_GODOT_OPTIONS+=( --single-window --disable-vsync --max-fps 30 @@ -127,10 +148,10 @@ case "$PERFORMANCE_PROFILE" in ;; esac -# NETfishing reads gameplay input natively. The explicit no-op map prevents +# 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 "NETfishing.aarch64" -c "$GPTOKEYB_CONFIG" & +$GPTOKEYB "straywild.aarch64" -c "$GPTOKEYB_CONFIG" & pm_platform_helper "$GAME_EXECUTABLE" $ESUDO env CRUSTY_RESOLUTION="${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \ @@ -139,12 +160,12 @@ $ESUDO env CRUSTY_RESOLUTION="${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \ XDG_CONFIG_HOME="$CONFDIR/config" \ XDG_CACHE_HOME="$CONFDIR/cache" \ GODOT_SILENCE_ROOT_WARNING=1 \ - "${NETFISHING_DATA_ENVIRONMENT[@]}" \ - "${NETFISHING_GAME_ENVIRONMENT[@]}" \ + "${straywild_DATA_ENVIRONMENT[@]}" \ + "${straywild_GAME_ENVIRONMENT[@]}" \ "$GAME_LAUNCHER" \ "$CONTROLLER_MAPPING_FILE" \ "$GAME_EXECUTABLE" \ - "${NETFISHING_GODOT_OPTIONS[@]}" \ + "${straywild_GODOT_OPTIONS[@]}" \ --resolution "${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \ --fullscreen \ --rendering-driver opengl3_es \ diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh index 2ebaeff..ff108a8 100755 --- a/scripts/run_validations.sh +++ b/scripts/run_validations.sh @@ -6,9 +6,9 @@ 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 netfishing-validations.XXXXXX)" +readonly RUN_ROOT="$(mktemp -d -t straywild-validations.XXXXXX)" -export NETFISHING_ISOLATED_VALIDATION_STORAGE=1 +export straywild_ISOLATED_VALIDATION_STORAGE=1 readonly -a QUICK_TESTS=( "scripts/validate_animalese_samples.gd" @@ -87,6 +87,7 @@ readonly -a NETWORK_TESTS=( "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" diff --git a/scripts/setup_android_build.sh b/scripts/setup_android_build.sh index c1cebdc..bc96083 100755 --- a/scripts/setup_android_build.sh +++ b/scripts/setup_android_build.sh @@ -336,8 +336,8 @@ check_editor_settings() { run_export_hint() { echo echo "Example export commands:" - echo " ${GODOT} --headless --path \"$PROJECT_ROOT\" --export-release \"Android\" builds/${PROJECT_VERSION}/android/NETfishing.apk" - echo " ${GODOT} --headless --path \"$PROJECT_ROOT\" --export-debug \"Android\" builds/android/NETfishing-debug.apk" + 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..." diff --git a/scripts/test_player_data_root_paths.gd b/scripts/test_player_data_root_paths.gd index 69a2485..f357b9a 100644 --- a/scripts/test_player_data_root_paths.gd +++ b/scripts/test_player_data_root_paths.gd @@ -1,36 +1,36 @@ extends SceneTree -const PROJECT_PATH := "/tmp/netfishing" -const INSTALL_PATH := "/opt/NETfishing" +const PROJECT_PATH := "/tmp/straywild" +const INSTALL_PATH := "/opt/straywild" func _initialize() -> void: - var root_to_create: String = OS.get_environment("NETFISHING_TEST_CREATE_ROOT") + 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/NETfishing-test-data", + "/tmp/straywild-test-data", PlayerDataRoot.PATH_ALLOWED, failures, ) _expect( "external trailing slash", - "/tmp/NETfishing-test-data/", + "/tmp/straywild-test-data/", PlayerDataRoot.PATH_ALLOWED, failures, ) _expect( "similar project-name sibling", - "/tmp/netfishing-data", + "/tmp/straywild-data", PlayerDataRoot.PATH_ALLOWED, failures, ) _expect( "documents directory", - "/home/player/Documents/NETfishing", + "/home/player/Documents/straywild", PlayerDataRoot.PATH_ALLOWED, failures, ) @@ -55,7 +55,7 @@ func _initialize() -> void: ) _expect_with_references( "empty project reference", - "/tmp/NETfishing-test-data", + "/tmp/straywild-test-data", "", INSTALL_PATH, PlayerDataRoot.PATH_ALLOWED, @@ -63,7 +63,7 @@ func _initialize() -> void: ) _expect_with_references( "root project reference", - "/tmp/NETfishing-test-data", + "/tmp/straywild-test-data", "/", INSTALL_PATH, PlayerDataRoot.PATH_ALLOWED, @@ -71,9 +71,9 @@ func _initialize() -> void: ) _expect_with_references( "Windows component and case normalization", - "C:\\Games\\NETfishing Data", - "C:\\Games\\NETfishing", - "C:\\Program Files\\NETfishing", + "C:\\Games\\straywild Data", + "C:\\Games\\straywild", + "C:\\Program Files\\straywild", PlayerDataRoot.PATH_ALLOWED, failures, true, diff --git a/server/dedicated_server_config.gd b/server/dedicated_server_config.gd index 2f515c4..3370457 100644 --- a/server/dedicated_server_config.gd +++ b/server/dedicated_server_config.gd @@ -1,7 +1,7 @@ class_name DedicatedServerConfig extends RefCounted -const DEFAULT_NAME: String = "NETfishing Dedicated Server" +const DEFAULT_NAME: String = "straywild Dedicated Server" const DEFAULT_BIND_ADDRESS: String = "*" const DEFAULT_PORT: int = 7777 const DEFAULT_MAX_PLAYERS: int = 8 @@ -31,8 +31,10 @@ static func from_runtime() -> DedicatedServerConfig: result.discovery_url = str(ProjectSettings.get_setting( "network/discovery/base_url", "" )).strip_edges() - var config_path: String = OS.get_environment( - "NETFISHING_SERVER_CONFIG" + 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="): @@ -87,36 +89,44 @@ func _load_file(path: String) -> bool: func _apply_environment() -> void: server_name = _environment_string( - "NETFISHING_SERVER_NAME", server_name + "straywild_SERVER_NAME", "NETFISHING_SERVER_NAME", server_name ) bind_address = _environment_string( - "NETFISHING_SERVER_BIND", bind_address + "straywild_SERVER_BIND", "NETFISHING_SERVER_BIND", bind_address + ) + port = _environment_int( + "straywild_SERVER_PORT", "NETFISHING_SERVER_PORT", port ) - port = _environment_int("NETFISHING_SERVER_PORT", port) max_players = _environment_int( - "NETFISHING_SERVER_MAX_PLAYERS", max_players + "straywild_SERVER_MAX_PLAYERS", + "NETFISHING_SERVER_MAX_PLAYERS", + max_players, ) world_seed = _environment_int( - "NETFISHING_WORLD_SEED", world_seed + "straywild_WORLD_SEED", "NETFISHING_WORLD_SEED", world_seed ) public_listing = _environment_bool( - "NETFISHING_SERVER_PUBLIC", public_listing + "straywild_SERVER_PUBLIC", "NETFISHING_SERVER_PUBLIC", public_listing ) discovery_url = _environment_string( - "NETFISHING_DISCOVERY_URL", discovery_url + "straywild_DISCOVERY_URL", "NETFISHING_DISCOVERY_URL", discovery_url ) data_directory = _environment_string( - "NETFISHING_DATA_DIR", data_directory + "straywild_DATA_DIR", "NETFISHING_DATA_DIR", data_directory ) chat_logging = _environment_bool( - "NETFISHING_CHAT_LOGGING", chat_logging + "straywild_CHAT_LOGGING", "NETFISHING_CHAT_LOGGING", chat_logging ) chat_log_path = _environment_string( - "NETFISHING_CHAT_LOG_PATH", chat_log_path + "straywild_CHAT_LOG_PATH", "NETFISHING_CHAT_LOG_PATH", chat_log_path ) - if OS.has_environment("NETFISHING_SERVER_OPERATORS"): + 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("NETFISHING_SERVER_OPERATORS") + OS.get_environment(operators_variable) ) @@ -211,21 +221,45 @@ static func _safe_text(value: String) -> bool: return true -static func _environment_string(name: String, fallback: String) -> String: - return OS.get_environment(name) if OS.has_environment(name) else fallback +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_int(name: String, fallback: int) -> int: +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(name), fallback) - if OS.has_environment(name) else fallback + _parse_int(OS.get_environment(variable), fallback) + if not variable.is_empty() else fallback ) -static func _environment_bool(name: String, fallback: bool) -> bool: - if not OS.has_environment(name): +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(name).strip_edges().to_lower() + var value: String = OS.get_environment(variable).strip_edges().to_lower() return value in ["1", "true", "yes", "on"] diff --git a/settings/player_settings_manager.gd b/settings/player_settings_manager.gd index 3fd2a35..362abc9 100644 --- a/settings/player_settings_manager.gd +++ b/settings/player_settings_manager.gd @@ -7,7 +7,7 @@ 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 = ( - "NETFISHING_ISOLATED_VALIDATION_STORAGE" + "straywild_ISOLATED_VALIDATION_STORAGE" ) signal settings_changed(settings: PlayerSettings) diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 3b9d6a3..146fb4e 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -19,9 +19,15 @@ func _run() -> void: 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()) + assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND)) main.call("_enter_gameplay") await physics_frame await physics_frame @@ -94,11 +100,7 @@ func _run() -> void: assert(keyboard_mapping.reset_mapping()) await process_frame var shop_prompt_key := game_ui.get_node("%ShopPromptKey") as Label - var storage_prompt_message := game_ui.get_node( - "%StoragePromptMessage" - ) as Label assert(shop_prompt_key.text == "E") - assert(storage_prompt_message.text == "E open storage") var remapped_interact := InputEventKey.new() remapped_interact.physical_keycode = KEY_F remapped_interact.pressed = true @@ -108,7 +110,6 @@ func _run() -> void: )) await process_frame assert(shop_prompt_key.text == "F") - assert(storage_prompt_message.text == "F open storage") assert(keyboard_mapping.reset_mapping()) await process_frame assert(toolbar.get_parent() == chat_ui.get_parent()) @@ -416,6 +417,12 @@ func _run() -> void: ) 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"))) diff --git a/tests/character_rig_validation.gd b/tests/character_rig_validation.gd index c8fc57d..2aece2b 100644 --- a/tests/character_rig_validation.gd +++ b/tests/character_rig_validation.gd @@ -2,7 +2,7 @@ extends SceneTree const CHARACTER_SCENE: PackedScene = preload( - "res://art/exported/characters/base/netfishing_base_character.glb" + "res://art/exported/characters/base/straywild_base_character.glb" ) const EXPECTED_ANIMATIONS: Array[StringName] = [ diff --git a/tests/controller_mapping_validation.gd b/tests/controller_mapping_validation.gd index 3a4645a..a5938d6 100644 --- a/tests/controller_mapping_validation.gd +++ b/tests/controller_mapping_validation.gd @@ -278,7 +278,7 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String: func _validate_portmaster_launcher() -> String: - const launcher_path: String = "res://scripts/portmaster/NETfishing.sh" + 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) @@ -291,9 +291,9 @@ func _validate_portmaster_launcher() -> String: "guide:b8,start:b7,back:b6", "leftstick:b9", "rightstick:b12", - "netfishing_controllerconfig=\"$GODOT_MUOS_MAPPING\"", - "GPTOKEYB_CONFIG=\"$GAMEDIR/netfishing.gptk\"", - "$GPTOKEYB \"NETfishing.aarch64\" -c \"$GPTOKEYB_CONFIG\" &", + "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\"", @@ -304,7 +304,7 @@ func _validate_portmaster_launcher() -> String: if "$'\\n'" in launcher: return "PortMaster launcher appends mappings across a Weston-unsafe newline" const game_launcher_path: String = ( - "res://scripts/portmaster/launch-netfishing.sh" + "res://scripts/portmaster/launch-straywild.sh" ) if not FileAccess.file_exists(game_launcher_path): return "PortMaster game launcher wrapper is missing" diff --git a/tests/controller_menu_accessibility_validation.gd b/tests/controller_menu_accessibility_validation.gd index 70d2095..37e43d5 100644 --- a/tests/controller_menu_accessibility_validation.gd +++ b/tests/controller_menu_accessibility_validation.gd @@ -122,8 +122,38 @@ func _validate_primary_menu_navigation() -> void: _expect( play_button.icon != null and play_button.icon.resource_path - == "res://ui/icons/main_menu/continue.png", - "The Play action does not use the original Continue icon.", + == "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 @@ -131,24 +161,29 @@ func _validate_primary_menu_navigation() -> void: "Legacy New/Delete title bubbles are still visible.", ) _assert_neighbor( - title.get_node("%JoinGameButton") as Control, - &"focus_neighbor_right", 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_left", - play_button, + &"focus_neighbor_bottom", + title.get_node("%CreditsButton") as Control, ) _assert_neighbor( title.get_node("%CreditsButton") as Control, - &"focus_neighbor_right", - play_button, - ) - _assert_neighbor( + &"focus_neighbor_bottom", title.get_node("%QuitButton") as Control, - &"focus_neighbor_left", - play_button, ) for title_control: Control in title_controls: _assert_directionally_reachable(title_control, title_controls) @@ -1196,12 +1231,12 @@ func _validate_profile_voice_navigation() -> void: 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("NETFISHING_PROFILE_VOICE_CAPTURE"): + 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( - "NETFISHING_PROFILE_VOICE_CAPTURE" + "straywild_PROFILE_VOICE_CAPTURE" )) == OK, "The profile voice-page capture could not be saved.", ) @@ -1321,6 +1356,24 @@ func _validate_player_menu_nested_controller_back() -> void: == 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) diff --git a/tests/controller_ui_navigation_validation.gd b/tests/controller_ui_navigation_validation.gd index e1169f9..24f39f9 100644 --- a/tests/controller_ui_navigation_validation.gd +++ b/tests/controller_ui_navigation_validation.gd @@ -266,22 +266,22 @@ func _validate_four_by_three_centering() -> void: func _validate_low_end_profile_contract() -> void: var launcher: String = FileAccess.get_file_as_string( - "res://scripts/portmaster/NETfishing.sh" + "res://scripts/portmaster/straywild.sh" ) - assert(launcher.contains("NETFISHING_PERFORMANCE_PROFILE:-")) + assert(launcher.contains("straywild_PERFORMANCE_PROFILE:-")) assert(launcher.contains("$CONFDIR/performance_profile")) assert(launcher.contains("normal)")) assert(launcher.contains("light|\"\")")) - assert(launcher.contains("NETFISHING_PERFORMANCE_PROFILE=normal")) - assert(launcher.contains("NETFISHING_PERFORMANCE_PROFILE=light")) - assert(launcher.contains("NETFISHING_LOW_END=1")) + 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("netfishing-portmaster-export")) + assert(builder.contains("straywild-portmaster-export")) var asset_profile: String = FileAccess.get_file_as_string( "res://scripts/prepare_portmaster_assets.sh" ) diff --git a/tests/dedicated_server_config_validation.gd b/tests/dedicated_server_config_validation.gd index 02a881d..2125220 100644 --- a/tests/dedicated_server_config_validation.gd +++ b/tests/dedicated_server_config_validation.gd @@ -15,7 +15,7 @@ func _initialize() -> void: func _run() -> void: var defaults := ConfigType.new() - defaults.set("data_directory", "/tmp/netfishing-server") + defaults.set("data_directory", "/tmp/straywild-server") defaults.call("_validate") assert(defaults.is_valid()) assert(not bool(defaults.get("public_listing"))) @@ -37,7 +37,7 @@ func _run() -> void: 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.netfishing.org/") + file.set_value("discovery", "url", "https://discovery.straywild.io/") file.set_value("privacy", "chat_logging", true) file.set_value( "privacy", @@ -59,7 +59,7 @@ func _run() -> void: 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.netfishing.org") + assert(str(configured.get("discovery_url")) == "https://discovery.straywild.io") assert(bool(configured.get("chat_logging"))) assert( str(configured.get("chat_log_path")) diff --git a/tests/digging_prototype_validation.gd b/tests/digging_prototype_validation.gd index 21f8629..95917a0 100644 --- a/tests/digging_prototype_validation.gd +++ b/tests/digging_prototype_validation.gd @@ -14,11 +14,11 @@ const Gatherables: GatherableCatalog = preload( class PrimaryTerrainProvider: extends Node3D - var primary_mesh: MeshInstance3D + var primary_meshes: Array[MeshInstance3D] = [] func get_primary_terrain_meshes() -> Array[MeshInstance3D]: - return [primary_mesh] if primary_mesh != null else [] + return primary_meshes func _initialize() -> void: @@ -98,9 +98,17 @@ func _validate_primary_terrain_provider() -> void: var provider := PrimaryTerrainProvider.new() provider.name = "Provider" fixture.add_child(provider) - provider.primary_mesh = _flat_sand_triangle(0.0) - provider.add_child(provider.primary_mesh) - var buried_base := _flat_sand_triangle(10.0) + 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() @@ -111,28 +119,34 @@ func _validate_primary_terrain_provider() -> void: 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() == 1) - var center := (triangles[0][0] + triangles[0][1] + triangles[0][2]) / 3.0 - assert(center.x < 2.0) + 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_sand_triangle(x_offset: float) -> MeshInstance3D: +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, 0.0, 0.0), - Vector3(x_offset, 0.0, 1.0), - Vector3(x_offset + 1.0, 0.0, 0.0), + 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 = "sand" + material.resource_name = str(material_name) mesh.surface_set_material(0, material) var mesh_instance := MeshInstance3D.new() - mesh_instance.name = "PrimarySand" + mesh_instance.name = "Primary%s" % str(material_name).capitalize() mesh_instance.mesh = mesh return mesh_instance diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index de0133b..0728d82 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -31,9 +31,14 @@ func _run() -> void: 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()) + assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND)) main.call("_enter_gameplay") for _frame: int in 8: await process_frame @@ -42,14 +47,13 @@ func _run() -> void: 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 session := main.get_node("%NetworkSession") as NetworkSession 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(LogbookCatalog.ordered_species(catalog.candidates).size() == 314) assert(sale_service != null) assert(shop_service != null) assert(session != null and session.is_host()) @@ -78,7 +82,7 @@ func _run() -> void: main, player, catalog, sale_service, reservations ) _test_host_backpack_purchase(player, shop_service) - _test_host_storage_purchase(player, shop_service) + await _test_host_home_upgrade(main, player, shop_service) assert(not sale_service.is_local_sale_pending()) assert(not shop_service.is_local_purchase_pending()) @@ -171,7 +175,6 @@ func _run_multiplayer_client() -> void: 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: @@ -198,7 +201,6 @@ func _run_multiplayer_client() -> void: 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 @@ -554,30 +556,58 @@ func _test_host_backpack_purchase( 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])) + 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( +func _test_host_home_upgrade( + main: Node, player: Player, shop_service: NetworkShopService, ) -> void: + var interaction := main.get( + "_rv_upgrade_shop_interaction" + ) as RVUpgradeInteraction + var home_state := main.get("_home_state") as PlayerHomeState var capacity := player.cooler_capacity + assert(interaction != null and home_state != null) + player.global_position = interaction.global_position + for _frame: int in 4: + await physics_frame + assert(interaction.is_avatar_in_range(player)) + assert(home_state.get_tier() == PlayerHomeState.STARTER_TIER) + assert( + interaction.get_dialogue_text(home_state.get_tier()) + == RVUpgradeInteraction.TENT_DIALOGUE_TEXT + ) 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 upgrade_cost: int = home_state.get_next_upgrade_cost() + assert(upgrade_cost == PlayerHomeState.HOME_UPGRADE_COSTS.front()) + assert(upgrade_cost == 50) + if player.wallet.get_balance() < upgrade_cost: + assert(player.wallet.credit( + upgrade_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])) - assert(capacity.get_capacity() == expected_capacity) - assert(player.wallet.get_balance() == balance_before - total_cost) + _shop_result.clear() + assert(not shop_service.request_home_upgrade().is_empty()) + assert( + not _shop_result.is_empty() and bool(_shop_result[1]), + "Home purchase failed: %s" % [_shop_result], + ) + assert(home_state.get_tier() == PlayerHomeState.STARTER_RV_TIER) + assert( + interaction.get_dialogue_text(home_state.get_tier()) + == RVUpgradeInteraction.RV_DIALOGUE_TEXT + ) + assert(capacity.get_capacity() == 18) + assert(player.wallet.get_balance() == balance_before - upgrade_cost) + assert(home_state.get_next_upgrade_cost() == -1) assert(capacity.get_next_cost() == -1) @@ -971,12 +1001,21 @@ func _test_fishing_shop_sale_ui( reserved_catch.catch_id, ) assert(staged.is_empty()) - sell_inventory.call( - "_stage", - PlayerInventoryLayout.EntryKind.CATCH, - fish_catch.catch_id, + 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 @@ -985,17 +1024,19 @@ func _test_fishing_shop_sale_ui( 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 sell_button := sell_inventory.get("_sell_button") as Button + 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() < sell_button.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 + fish_catch.sale_value) + 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) diff --git a/tests/fish_catalog_content_validation.gd b/tests/fish_catalog_content_validation.gd index 3b31005..82f5604 100644 --- a/tests/fish_catalog_content_validation.gd +++ b/tests/fish_catalog_content_validation.gd @@ -40,39 +40,17 @@ const StarterRegionScene = preload( 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 FINAL_CREATURE_CATALOG_NUMBERS: Array[int] = [ + 101, 102, 103, 104, 105, 106, 107, 201, 403, 701, 704, 801, + 802, 805, 806, 808, 2502, 2504, 2505, 2508, 2601, 2801, 2802, + 2803, 2804, 2806, 2807, 3002, 3102, 3103, 3104, 3107, 3201, + 3401, 3902, 3906, 4101, 4401, 4701, 5001, 5702, 5901, 5902, + 5903, 5904, 6001, 6103, 6406, 6601, 6801, 6804, 6807, 6901, + 6902, 6904, 6905, 7101, 7403, 7506, 7508, +] -const ACTIVE_CATALOG_COUNT: int = 313 -const INACTIVE_CATALOG_COUNT: int = 3 +const ACTIVE_CATALOG_COUNT: int = 314 +const INACTIVE_CATALOG_COUNT: int = 2 const FISHING_SPECIES_COUNT: int = 310 const GENERATED_POND_COUNT: int = 33 const GENERATED_LAKE_COUNT: int = 20 @@ -235,10 +213,15 @@ func _validate_catalog_and_pools() -> void: else: inactive_count += 1 assert(not fish.is_selectable()) - assert(fish.display_texture == null) - if FINAL_CREATURE_TEXTURE_PATHS.has(fish.id): + if fish.catalog_number not in FINAL_CREATURE_CATALOG_NUMBERS: + assert(fish.display_texture == null) + if fish.catalog_number in FINAL_CREATURE_CATALOG_NUMBERS: + var expected_path := ( + "res://art/exported/creatures/%d.png" + % fish.catalog_number + ) var expected_texture: Texture2D = load( - FINAL_CREATURE_TEXTURE_PATHS[fish.id] + expected_path ) as Texture2D assert(expected_texture != null) assert(fish.display_texture == expected_texture) @@ -246,14 +229,14 @@ func _validate_catalog_and_pools() -> void: if fish.collection_method == FishDataType.CollectionMethod.FISHING: fishing_count += 1 assert(fish.active) - if not FINAL_CREATURE_TEXTURE_PATHS.has(fish.id): + if fish.catalog_number not in FINAL_CREATURE_CATALOG_NUMBERS: 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) + assert(final_art_count == FINAL_CREATURE_CATALOG_NUMBERS.size()) + assert(placeholder_fishing_count == 253) var chum: FishDataType = Catalog.get_fish_by_id(&"salmon_chum") assert(chum != null) assert(chum.get_season_text() == "fall") @@ -291,7 +274,7 @@ func _validate_catalog_and_pools() -> void: ) == chum ) seasonal_collection.free() - var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"crab_blue") + var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"crab_dungeness") assert(inactive_fish != null and not inactive_fish.active) var inactive_pool := FishPoolType.new() inactive_pool.candidates = [inactive_fish] diff --git a/tests/fish_hotbar_showcase_validation.gd b/tests/fish_hotbar_showcase_validation.gd index c2d687c..af3644b 100644 --- a/tests/fish_hotbar_showcase_validation.gd +++ b/tests/fish_hotbar_showcase_validation.gd @@ -104,7 +104,10 @@ func _run() -> void: 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"]) == 10) + assert(int(parsed["save_version"]) == 11) + assert(not PlayerHomeState.sanitize_save_data( + parsed.get("home", {}) + ).is_empty()) assert( int(parsed["experience"]["total_experience"]) == 125 @@ -197,8 +200,8 @@ func _run() -> void: 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 == 10) - assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17) + assert(NetworkProtocol.PROTOCOL_VERSION == 12) + assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18) assert( NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1" diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd index 3441344..99a2169 100644 --- a/tests/fish_quality_validation.gd +++ b/tests/fish_quality_validation.gd @@ -26,8 +26,8 @@ func _run() -> void: _validate_collection_mastery() _validate_version_four_migration() _validate_fishing_protocol_v2() - assert(NetworkProtocol.PROTOCOL_VERSION == 10) - assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17) + assert(NetworkProtocol.PROTOCOL_VERSION == 12) + assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18) assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11) assert( NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY @@ -485,7 +485,7 @@ func _validate_version_four_migration() -> void: version_four, 4, ) - assert(int(migrated.get("save_version", -1)) == 10) + assert(int(migrated.get("save_version", -1)) == 11) assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0) assert( is_equal_approx( @@ -509,4 +509,7 @@ func _validate_version_four_migration() -> void: 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/fishing_authority_validation.gd b/tests/fishing_authority_validation.gd index e7c87bb..b3c17dd 100644 --- a/tests/fishing_authority_validation.gd +++ b/tests/fishing_authority_validation.gd @@ -265,6 +265,15 @@ func _run() -> void: 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()) diff --git a/tests/fishing_multiplayer_validation.gd b/tests/fishing_multiplayer_validation.gd index 73de1bd..9d7ef7f 100644 --- a/tests/fishing_multiplayer_validation.gd +++ b/tests/fishing_multiplayer_validation.gd @@ -63,12 +63,38 @@ func _run_host() -> void: 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) - remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1) - var remote_visuals := remote_avatar.get_node("Visuals") as Node3D - remote_visuals.rotation.y = PI * 0.5 + 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 ( @@ -82,7 +108,7 @@ func _run_host() -> void: == Player.FishingVisualPhase.CASTING ) var remote_position_error := ( - remote_avatar.global_position - Vector3(-0.5, 3.95, 2.1) + remote_avatar.global_position - expected_position ) remote_position_error.y = 0.0 assert( @@ -90,7 +116,7 @@ func _run_host() -> void: "remote casting position drifted: %s" % remote_avatar.global_position ) assert( - remote_avatar.get_facing_direction().dot(Vector3.LEFT) > 0.99, + remote_avatar.get_facing_direction().dot(expected_direction) > 0.99, "remote casting facing drifted: %s" % remote_avatar.get_facing_direction() ) @@ -196,6 +222,23 @@ func _run_client() -> void: 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" @@ -206,7 +249,6 @@ func _run_client() -> void: cast_rejections.append(message) ) var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType - var player := main.get("_player") as Player 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: @@ -217,15 +259,20 @@ func _run_client() -> void: "Visuals/CharacterRig/AnimationPlayer" ) as AnimationPlayer assert(character_animation_player.has_animation(&"retract_sit")) - var placement_deadline: int = Time.get_ticks_msec() + 5000 + # 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(Vector3(-0.5, 3.95, 2.1)) - > 0.25 + and player.global_position.distance_to(exterior_spawn.origin) <= 1.0 ): await physics_frame - assert(player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1)) <= 0.25) - assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99) + 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( @@ -241,7 +288,10 @@ func _run_client() -> void: 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")) @@ -250,6 +300,7 @@ func _run_client() -> void: 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) @@ -258,14 +309,33 @@ func _run_client() -> void: 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 fishing_spot.is_cast_path_clear(cast_origin, candidate_target) + 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") + 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) @@ -388,3 +458,79 @@ func _create_initialized_main() -> Node: 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_surface_validation.gd b/tests/fishing_surface_validation.gd index eb02c6c..8248c36 100644 --- a/tests/fishing_surface_validation.gd +++ b/tests/fishing_surface_validation.gd @@ -156,6 +156,7 @@ func _validate_portable_water_body() -> void: 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 @@ -183,6 +184,34 @@ func _validate_portable_water_body() -> void: == 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 diff --git a/tests/gathering_showcase_validation.gd b/tests/gathering_showcase_validation.gd index 7099f7c..51d6f38 100644 --- a/tests/gathering_showcase_validation.gd +++ b/tests/gathering_showcase_validation.gd @@ -21,9 +21,13 @@ func _run() -> void: 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()) + assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND)) main.call("_enter_gameplay") for _frame: int in 8: await process_frame @@ -38,7 +42,7 @@ func _run() -> void: and game_ui != null and catalog != null ) - fishing_spot.minimum_showcase_duration = 0.15 + 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)) @@ -81,7 +85,7 @@ func _run() -> void: await process_frame assert(fishing_spot.get("_pending_catch") == crab_catch) Input.action_release("fish_primary") - await create_timer(0.2).timeout + 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) diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index 0143322..076f1c9 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -12,6 +12,9 @@ const GeneratedLakePool: FishPool = preload( const GeneratedRiverPool: FishPool = preload( "res://fish/pools/generated_river_pool.tres" ) +const Gatherables: GatherableCatalog = preload( + "res://gathering/catalog/gatherable_catalog.tres" +) const PrecipitationOcclusionType = preload( "res://world/environment/precipitation_occlusion.gd" ) @@ -402,7 +405,7 @@ func _validate_generated_region( + generator.stacked_elevated_placement_keys().size() ) ) - _validate_elevated_cliff_feature(generator) + _validate_elevated_cliff_feature(region, generator) var shop := region.get_node("Interactables/FishingShopWorld") as Node3D var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D @@ -490,6 +493,10 @@ func _validate_generated_region( == 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( @@ -532,6 +539,12 @@ func _validate_generated_region( -INF, 0.35, ) + _validate_crab_spawn_surfaces(region) + _validate_water_recovery_positions( + region, + grass_prop_surface_triangles, + sand_prop_surface_triangles, + ) var anchors := region.get_node( "GatherableAnchors/ReachableTreeTrunks" ) as GatherableAnchorSet3D @@ -682,6 +695,7 @@ func _validate_generated_region( func _validate_elevated_cliff_feature( + region: GeneratedWorldRegion, generator: TerrainChunkGenerator, ) -> void: var elevated_coordinates: Dictionary[Vector2i, StringName] = {} @@ -815,6 +829,27 @@ func _validate_elevated_cliff_feature( or generator.placement_keys()[low_index].begins_with("chunk_spawn@") ) assert(found_primary_ramp == not primary_is_third_tier) + var primary_surface_height := ( + generator.elevated_cliff_level_height + * (2.0 if primary_is_third_tier else 1.0) + ) + var primary_surface_position := generator.to_global( + generator.chunk_position(feature_center) + + Vector3.UP * primary_surface_height + ) + assert( + region.is_home_exterior_position_accessible(primary_surface_position) + == not primary_is_third_tier + ) + assert(region.is_home_exterior_position_accessible(generator.to_global( + generator.chunk_position(feature_center) + ))) + # A higher surface sharing a ramp-accessible base cell is still invalid + # unless that elevation has its own authored ramp connection. + assert(not region.is_home_exterior_position_accessible(generator.to_global( + generator.chunk_position(feature_center) + + Vector3.UP * (generator.elevated_cliff_level_height * 2.0) + ))) var generated := generator.get_generated_chunks_root() assert(generated != null) @@ -868,7 +903,7 @@ func _validate_elevated_cliff_feature( var overlay_mesh := TerrainChunkAnalyzer.find_primary_mesh( overlay, ( - &"chunk_0015" + &"chunk_0010" if stable_id in [ &"chunk_9999", &"chunk_9998", @@ -1043,6 +1078,25 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void: 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") @@ -1139,6 +1193,20 @@ func _validate_decoration_transform( 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( @@ -1209,6 +1277,58 @@ func _validate_decoration_transform( ) +func _validate_crab_spawn_surfaces(region: GeneratedWorldRegion) -> void: + var brown := Gatherables.get_entry(&"crab_brown") + var blue := Gatherables.get_entry(&"crab_blue") + assert(brown != null and blue != null) + assert(is_equal_approx( + region.get_saltwater_surface_height(), + GeneratedWorldRegion.WATER_HEIGHT, + )) + var brown_minimum := maxf( + brown.minimum_surface_y, + GeneratedWorldRegion.WATER_HEIGHT + brown.waterline_margin, + ) + var blue_maximum := minf( + blue.maximum_surface_y, + GeneratedWorldRegion.WATER_HEIGHT - blue.waterline_margin, + ) + assert(brown_minimum > blue_maximum) + var brown_triangles := region.get_spawn_surface_triangles( + brown.surface_materials, + brown_minimum, + 0.6, + brown.maximum_surface_y, + ) + var blue_triangles := region.get_spawn_surface_triangles( + blue.surface_materials, + blue.minimum_surface_y, + 0.6, + blue_maximum, + ) + assert(not brown_triangles.is_empty()) + assert(not blue_triangles.is_empty()) + assert( + brown.target_population_for_surface_area( + _surface_triangle_area(brown_triangles) + ) > brown.population + ) + assert( + blue.target_population_for_surface_area( + _surface_triangle_area(blue_triangles) + ) > blue.population + ) + + +func _surface_triangle_area(triangles: Array[PackedVector3Array]) -> float: + var area := 0.0 + for triangle: PackedVector3Array in triangles: + area += (triangle[1] - triangle[0]).cross( + triangle[2] - triangle[0] + ).length() * 0.5 + return area + + func _triangle_surface_height_at( position: Vector3, triangles: Array[PackedVector3Array], @@ -1231,6 +1351,76 @@ func _triangle_surface_height_at( 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, diff --git a/tests/home_spawn_multiplayer_validation.gd b/tests/home_spawn_multiplayer_validation.gd new file mode 100644 index 0000000..2855eb2 --- /dev/null +++ b/tests/home_spawn_multiplayer_validation.gd @@ -0,0 +1,201 @@ +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(_faces_away_from_home(host_avatar, home_world, host_fingerprint)) + 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)) + assert(_faces_away_from_home(host_avatar, home_world, host_fingerprint)) + + 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 + )) + assert(_faces_away_from_home( + remote_avatar, home_world, remote_fingerprint + )) + + 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 + )) + assert(_faces_away_from_home(player, home_world, fingerprint)) + 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 _faces_away_from_home( + avatar: Player, + home_world: HomeWorldService, + fingerprint: String, +) -> bool: + var exterior_basis: Basis = home_world.get_exterior_transform( + fingerprint + ).basis + var outward_direction := exterior_basis.z + outward_direction.y = 0.0 + if outward_direction.is_zero_approx(): + return false + return avatar.get_facing_direction().dot( + outward_direction.normalized() + ) >= 0.99 + + +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 new file mode 100644 index 0000000..06150f2 --- /dev/null +++ b/tests/home_spawn_multiplayer_validation.gd.uid @@ -0,0 +1 @@ +uid://bndtkip5p6xma diff --git a/tests/inventory_notepad_art_validation.gd b/tests/inventory_notepad_art_validation.gd index c42883c..d871b49 100644 --- a/tests/inventory_notepad_art_validation.gd +++ b/tests/inventory_notepad_art_validation.gd @@ -666,7 +666,7 @@ func _to_screen_rect( func _capture_inventory_pages(player_menu: PlayerMenu) -> void: - if not OS.has_environment("NETFISHING_NOTEPAD_CAPTURE"): + if not OS.has_environment("straywild_NOTEPAD_CAPTURE"): return player_menu.visible = true var sections: Array[PlayerMenu.Section] = [ @@ -700,7 +700,7 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void: func _save_capture(suffix: String) -> void: var image: Image = root.get_texture().get_image() var path: String = "%s-%s.png" % [ - OS.get_environment("NETFISHING_NOTEPAD_CAPTURE"), + OS.get_environment("straywild_NOTEPAD_CAPTURE"), suffix, ] assert(image.save_png(path) == OK) diff --git a/tests/job_system_validation.gd b/tests/job_system_validation.gd index 1fe5ceb..65b7164 100644 --- a/tests/job_system_validation.gd +++ b/tests/job_system_validation.gd @@ -21,13 +21,17 @@ func _run() -> void: 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()) + assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND)) main.call("_enter_gameplay") await physics_frame await physics_frame @@ -205,8 +209,11 @@ func _run() -> void: ) assert(bool(decoded.get("ok", false))) var save_data: Dictionary = decoded["data"] - assert(int(save_data.get("save_version", -1)) == 10) + 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() diff --git a/tests/late_join_multiplayer_validation.gd b/tests/late_join_multiplayer_validation.gd index 3813486..6ea544f 100644 --- a/tests/late_join_multiplayer_validation.gd +++ b/tests/late_join_multiplayer_validation.gd @@ -90,11 +90,12 @@ func _run_first_client() -> void: player.toggle_sitting() assert(player.is_sitting()) assert(player.bag.add_item(&"crab_net")) - assert(player.hotbar.assign_item(0, &"crab_net")) - assert(player.hotbar.select_slot(0)) - for _frame: int in 2: - await process_frame - assert(bool(player.get("_active_item_is_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) @@ -185,6 +186,29 @@ func _wait_for_registry_size(session: NetworkSession, expected: int) -> bool: 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: diff --git a/tests/light_performance_profile_validation.gd b/tests/light_performance_profile_validation.gd index f56db35..6a654f1 100644 --- a/tests/light_performance_profile_validation.gd +++ b/tests/light_performance_profile_validation.gd @@ -123,7 +123,6 @@ func _validate_main_profile(main: Node) -> void: var game_ui := main.get_node("%GameUI") as GameUI var title_screen := game_ui.get_title_screen() assert(title_screen != null) - assert(not title_screen.is_decorative_presentation_active()) var player_menu := game_ui.get("_player_menu") as PlayerMenu assert(player_menu != null) assert(not player_menu.is_cooler_water_effect_enabled()) @@ -159,8 +158,6 @@ func _validate_normal_profile(main: Node) -> void: var game_ui := main.get_node("%GameUI") as GameUI var title_screen := game_ui.get_title_screen() assert(title_screen != null) - assert(title_screen.is_decorative_presentation_active()) - assert(title_screen.is_decorative_bubble_scheduler_active()) var player_menu := game_ui.get("_player_menu") as PlayerMenu assert(player_menu != null) assert(player_menu.is_cooler_water_effect_enabled()) diff --git a/tests/logbook_runtime_validation.gd b/tests/logbook_runtime_validation.gd index 8b19f92..e33e551 100644 --- a/tests/logbook_runtime_validation.gd +++ b/tests/logbook_runtime_validation.gd @@ -2,6 +2,7 @@ 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: @@ -36,12 +37,16 @@ func _run() -> void: 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(2.2).timeout + await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout assert(player_menu.visible) assert(logbook.visible) assert(backdrop.visible) @@ -51,6 +56,18 @@ func _run() -> void: 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 ) @@ -68,13 +85,29 @@ func _run() -> void: 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(2.2).timeout + 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(2.2).timeout + await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout player_menu.call( "_show_section_immediate", PlayerMenu.Section.PROFILE ) @@ -110,7 +143,7 @@ func _validate_save_round_trip( assert(catalog != null) assert(catalog.candidates.size() == 316) var active_species := LogbookCatalog.ordered_species(catalog.candidates) - assert(active_species.size() == 313) + assert(active_species.size() == 314) # 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] = [] @@ -131,12 +164,14 @@ func _validate_save_round_trip( player.inventory_layout.restore_backpack_level( PlayerInventoryLayout.MAX_BACKPACK_LEVEL ) - player.cooler_capacity.restore_level(PlayerCoolerCapacity.MAX_LEVEL) + var home_state := main.get("_home_state") as PlayerHomeState + assert(home_state != null) + assert(home_state.upgrade_to_next_tier()) assert( player.inventory_layout.get_backpack_level() == PlayerInventoryLayout.MAX_BACKPACK_LEVEL ) - assert(player.cooler_capacity.get_level() == PlayerCoolerCapacity.MAX_LEVEL) + assert(player.cooler_capacity.get_capacity() == 18) player.inventory.set_inventory_layout(null) for index: int in 4: _add_test_catch(player, round_trip_species[index]) @@ -201,13 +236,13 @@ func _add_test_catch(player: Player, fish: FishData) -> void: func _capture_if_requested(suffix: String) -> void: - if not OS.has_environment("NETFISHING_LOGBOOK_CAPTURE"): + 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("NETFISHING_LOGBOOK_CAPTURE") + OS.get_environment("straywild_LOGBOOK_CAPTURE") + suffix + ".png" ) diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd index daa728e..67747a5 100644 --- a/tests/logbook_validation.gd +++ b/tests/logbook_validation.gd @@ -17,17 +17,19 @@ 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("NETFISHING_TEST_CREATE_ROOT"): + 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("NETFISHING_TEST_CREATE_ROOT") + OS.get_environment("straywild_TEST_CREATE_ROOT") ) assert(bool(result.get("ok", false))) data_root.queue_free() @@ -35,6 +37,7 @@ func _run() -> void: quit() return _validate_catalog() + _validate_catalog_query() await _validate_page() print("Logbook validation: PASS") quit() @@ -43,7 +46,7 @@ func _run() -> void: func _validate_catalog() -> void: assert(CatalogResource.candidates.size() == 316) var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates) - assert(ordered.size() == 313) + assert(ordered.size() == 314) var previous_number: int = 0 var catalog_numbers: Dictionary[int, bool] = {} for fish: FishDataType in CatalogResource.candidates: @@ -68,6 +71,348 @@ func _validate_catalog() -> void: ) +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() @@ -91,6 +436,10 @@ func _validate_page() -> void: 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) @@ -103,6 +452,9 @@ func _validate_page() -> void: 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, @@ -155,6 +507,39 @@ func _validate_page() -> void: ) 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" @@ -255,7 +640,10 @@ func _validate_page() -> void: 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).size() == 3) + assert( + (page.get("_entry_buttons") as Dictionary).has(&"unknown_3902") + ) assert( (page.get("_entry_buttons") as Dictionary).has(&"unknown_3906") ) @@ -455,8 +843,19 @@ func _validate_page() -> void: (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() @@ -471,6 +870,54 @@ func _detail_text(page: LogbookPage) -> String: 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): diff --git a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn b/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn index df3a835..5c1e046 100644 --- a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn +++ b/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn @@ -4,7 +4,7 @@ [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/netfishing_environment.tres" id="5_environment"] +[ext_resource type="Environment" path="res://world/environment/straywild_environment.tres" id="5_environment"] [node name="TerrainChunkGeneratorTest" type="Node3D"] script = ExtResource("1_test") diff --git a/tests/movement_multiplayer_validation.gd b/tests/movement_multiplayer_validation.gd index c9af294..05a668d 100644 --- a/tests/movement_multiplayer_validation.gd +++ b/tests/movement_multiplayer_validation.gd @@ -37,7 +37,7 @@ func _validate_latency_smoothing() -> void: _validate_compact_animation_encoding() _validate_animation_action_ordering(avatar) _validate_transit_estimation() - _validate_remote_snapshot_smoothing(avatar) + await _validate_remote_snapshot_smoothing(avatar) _validate_remote_locomotion_playback_recovery(avatar) _validate_reliable_jump_intent(avatar) _validate_airborne_sitting(avatar) @@ -145,11 +145,20 @@ func _validate_compact_animation_encoding() -> void: 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. @@ -304,8 +313,24 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void: ) 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, @@ -358,6 +383,42 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void: 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) @@ -373,6 +434,45 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void: 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) @@ -493,15 +593,22 @@ 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(session.set_host_world( + assert(bool(main.call( + "_apply_world", WorldLayout.STARTER_ISLAND, NetworkProtocol.DEFAULT_WORLD_SEED, - )) - assert(session.start_private_host(TEST_PORT)) + 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 @@ -513,31 +620,40 @@ func _run_host() -> void: # 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) - player.apply_authoritative_network_input(_movement_input(1, true)) - await create_timer(2.5).timeout - player.apply_authoritative_network_input(_movement_input(2, false)) - await create_timer(2.5).timeout - player.apply_authoritative_network_input( - _movement_input(3, false, true, 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 create_timer(2.0).timeout - player.apply_authoritative_network_input( - _movement_input(4, false, false, true) + await _reset_locomotion_test_position(player, session, world) + input_sequence = await _sustain_locomotion_input( + player, input_sequence, 1.5, false ) - await create_timer(2.0).timeout - player.apply_authoritative_network_input( - _movement_input(5, false, false, true, &"draw", 1) + 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(6, false, false, true, &"strike", 2, true) + _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(7, false, false, false, &"", 3) + _movement_input(input_sequence, false, false, false, &"", 3) ) - var disconnect_deadline: int = Time.get_ticks_msec() + 8000 + var disconnect_deadline: int = Time.get_ticks_msec() + 15000 while ( Time.get_ticks_msec() < disconnect_deadline and session.is_authenticated_peer(remote_peer_id) @@ -586,12 +702,27 @@ func _run_client() -> void: 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() + 18000 + 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" @@ -632,15 +763,44 @@ func _run_client() -> void: and saw_dust ): break - assert(saw_running) - assert(saw_walking) - assert(saw_sneaking) - assert(saw_idle_sneak) - assert(saw_net_draw) - assert(saw_net_strike) - assert(saw_net_strike_paused) - assert(saw_animation_advance) - assert(saw_dust) + 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() @@ -678,6 +838,39 @@ func _movement_input( } +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() diff --git a/tests/net_attachment_validation.gd b/tests/net_attachment_validation.gd index 15b8b7d..6b53c08 100644 --- a/tests/net_attachment_validation.gd +++ b/tests/net_attachment_validation.gd @@ -57,23 +57,73 @@ func _run() -> void: "Net rear face did not follow the socket direction guide.", ) - var handle := catching_net.get_node_or_null("Handle") as MeshInstance3D - var hoop := catching_net.get_node_or_null("Hoop") as MeshInstance3D - var bag := catching_net.get_node_or_null("Bag") as MeshInstance3D - var impact_marker := ( - catching_net.get_node_or_null("ImpactMarker") as Marker3D + 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(handle != null and handle.mesh != null, "Placeholder handle is missing.") - _check(hoop != null and hoop.mesh != null, "Placeholder hoop is missing.") - _check(bag != null and bag.mesh != null, "Placeholder net bag is missing.") - _check(impact_marker != null, "Net impact marker is missing.") - if impact_marker != null: + _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( - attachment.get_impact_world_position().is_equal_approx( - impact_marker.global_position - ), - "Net impact position moved away from its marker.", + 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 diff --git a/tests/player_experience_validation.gd b/tests/player_experience_validation.gd index 5e9dbef..a76a3dd 100644 --- a/tests/player_experience_validation.gd +++ b/tests/player_experience_validation.gd @@ -143,7 +143,7 @@ func _validate_save_migration() -> void: version_five, 5, ) - assert(int(migrated.get("save_version", -1)) == 10) + 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", {}) @@ -155,6 +155,9 @@ func _validate_save_migration() -> void: 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/progression_archive_validation.gd b/tests/progression_archive_validation.gd index 7f98066..30f0df6 100644 --- a/tests/progression_archive_validation.gd +++ b/tests/progression_archive_validation.gd @@ -22,11 +22,24 @@ func _run() -> void: 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 - assert(save_manager != null and player != null and data_root != null) + 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")) @@ -38,7 +51,23 @@ func _run() -> void: _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"]) == 10) + 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" @@ -65,6 +94,8 @@ func _run() -> void: 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"] @@ -135,6 +166,9 @@ func _run() -> void: 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 := ( @@ -169,11 +203,14 @@ func _run() -> void: var migrated_root: String = data_root.root_path.get_base_dir().path_join( "progression-migrated-data" ) - var data_migration: Dictionary = PortableDataMigration.migrate_active_to( - data_root, migrated_root + 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." ) - assert(bool(data_migration.get("ok", false))) - assert(bool(data_migration.get("changed", false))) var migrated_save: String = migrated_root.path_join( "player/player_save.nfsave" ) @@ -193,6 +230,18 @@ func _run() -> void: "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: @@ -202,6 +251,124 @@ func _run() -> void: 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()) diff --git a/tests/surface_drawing_multiplayer_validation.gd b/tests/surface_drawing_multiplayer_validation.gd index d0508b2..f79700e 100644 --- a/tests/surface_drawing_multiplayer_validation.gd +++ b/tests/surface_drawing_multiplayer_validation.gd @@ -27,10 +27,12 @@ func _run_host() -> void: 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(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()) + 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 @@ -62,6 +64,40 @@ func _run_host() -> void: 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 )) @@ -152,6 +188,9 @@ func _run_host() -> void: ): 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 @@ -221,6 +260,20 @@ func _run_client() -> void: 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, @@ -381,3 +434,18 @@ func _create_initialized_main() -> Node: 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_runtime_validation.gd b/tests/surface_drawing_runtime_validation.gd index e04afc2..85110cd 100644 --- a/tests/surface_drawing_runtime_validation.gd +++ b/tests/surface_drawing_runtime_validation.gd @@ -20,9 +20,13 @@ func _run() -> void: 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()) + assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND)) main.call("_enter_gameplay") await physics_frame await physics_frame diff --git a/tests/terrain_chunk_generator_validation.gd b/tests/terrain_chunk_generator_validation.gd index a46d1d7..ec736d9 100644 --- a/tests/terrain_chunk_generator_validation.gd +++ b/tests/terrain_chunk_generator_validation.gd @@ -629,13 +629,14 @@ func _validate_catalog() -> void: ]: _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 deep corner " + "Each raised-coast endpoint must layer the inland corner " + "over an authored level-one coastline." ), ) @@ -657,6 +658,7 @@ func _validate_catalog() -> void: ]: _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 diff --git a/tests/title_credits_validation.gd b/tests/title_credits_validation.gd index 5bbf632..e5ac4cc 100644 --- a/tests/title_credits_validation.gd +++ b/tests/title_credits_validation.gd @@ -21,6 +21,10 @@ func _run() -> void: 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 @@ -34,9 +38,32 @@ func _run() -> void: 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 bubble uses its authored icon", + "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") @@ -44,29 +71,39 @@ func _run() -> void: Rect2(Vector2.ZERO, bubble_field.size).encloses( Rect2(credits_button.position, credits_button.size) ), - "credits bubble stays inside the authored title field", + "credits button stays inside the authored title field", ) _expect( credits_button.mouse_filter == Control.MOUSE_FILTER_STOP, - "credits bubble accepts pointer input", + "credits button accepts pointer input", ) - if OS.has_environment("NETFISHING_TITLE_MENU_CAPTURE"): + 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("NETFISHING_TITLE_MENU_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", @@ -80,11 +117,11 @@ func _run() -> void: back_button.mouse_filter == Control.MOUSE_FILTER_STOP, "credits back button accepts pointer clicks", ) - if OS.has_environment("NETFISHING_CREDITS_CAPTURE"): + if OS.has_environment("straywild_CREDITS_CAPTURE"): await process_frame _expect( _save_capture( - OS.get_environment("NETFISHING_CREDITS_CAPTURE") + OS.get_environment("straywild_CREDITS_CAPTURE") ) == OK, "credits validation capture saves successfully", ) @@ -97,6 +134,30 @@ func _run() -> void: "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() @@ -134,7 +195,7 @@ func _run() -> void: _expect(not credits_page.visible, "ui cancel closes credits") _expect( root.gui_get_focus_owner() == credits_button, - "navigation return restores focus to the credits bubble", + "navigation return restores focus to the credits button", ) var chillnfill_name := credits_page.get_node( diff --git a/tests/ui_scaling_runtime_validation.gd b/tests/ui_scaling_runtime_validation.gd index b9e8bce..6e6182a 100644 --- a/tests/ui_scaling_runtime_validation.gd +++ b/tests/ui_scaling_runtime_validation.gd @@ -66,12 +66,6 @@ func _run() -> void: var pause_profile_stage := pause_menu.get_node( "ResponsivePauseStage" ) as Control - var decorative_fish_layer := title_screen.get_node( - "DecorativeFishLayer" - ) as Control - var decorative_bubble_layer := title_screen.get_node( - "DecorativeBubbleLayer" - ) as Control ui_viewport.add_child(game_ui) presenter.add_child(ui_viewport) root.add_child(presenter) @@ -165,26 +159,6 @@ func _run() -> void: assert(pause_profile_stage.position.is_equal_approx( expected_profile_position )) - assert(decorative_fish_layer.size.is_equal_approx( - expected_visible_size - )) - assert(decorative_bubble_layer.size.is_equal_approx( - expected_visible_size - )) - var left_to_right_bounds: Vector2 = ( - TitleScreen.get_decorative_fish_crossing_bounds( - expected_visible_size.x, 160.0, 1.0 - ) - ) - var right_to_left_bounds: Vector2 = ( - TitleScreen.get_decorative_fish_crossing_bounds( - expected_visible_size.x, 160.0, -1.0 - ) - ) - assert(left_to_right_bounds.x < -160.0) - assert(left_to_right_bounds.y > expected_visible_size.x) - assert(right_to_left_bounds.x > expected_visible_size.x) - assert(right_to_left_bounds.y < -160.0) 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 @@ -310,13 +284,13 @@ func _run() -> void: chat_panel.position.x * effective_scale, ] ) - if OS.has_environment("NETFISHING_UI_SCALE_CAPTURE"): + 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("NETFISHING_UI_SCALE_CAPTURE"), + OS.get_environment("straywild_UI_SCALE_CAPTURE"), target_size.x, target_size.y, actual_size.x, diff --git a/tests/unified_inventory_validation.gd b/tests/unified_inventory_validation.gd index e9a0d85..6928969 100644 --- a/tests/unified_inventory_validation.gd +++ b/tests/unified_inventory_validation.gd @@ -35,6 +35,41 @@ func _run() -> void: 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) diff --git a/tests/world_layout_validation.gd b/tests/world_layout_validation.gd index dd40fcf..e6eec39 100644 --- a/tests/world_layout_validation.gd +++ b/tests/world_layout_validation.gd @@ -100,6 +100,14 @@ func _validate_world_switching() -> void: 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) diff --git a/tests/world_spawn_multiplayer_validation.gd b/tests/world_spawn_multiplayer_validation.gd index 45be081..f5ffd2e 100644 --- a/tests/world_spawn_multiplayer_validation.gd +++ b/tests/world_spawn_multiplayer_validation.gd @@ -2,9 +2,10 @@ extends SceneTree const MainScene: PackedScene = preload("res://main/main.tscn") const TEST_PORT: int = 18170 -const EXPECTED_POPULATION: int = 8 +const EXPECTED_POPULATION: int = 12 const EXPECTED_BY_TYPE: Dictionary = { &"crab_brown": 2, + &"crab_blue": 4, &"clam_manila": 3, &"beetle_stag_common": 3, } @@ -161,6 +162,10 @@ func _validate_population( (position as Vector3).y >= entry.minimum_surface_y - 0.001 ) + if entry.type_id == &"crab_brown": + assert((position as Vector3).y > -0.45) + elif entry.type_id == &"crab_blue": + assert((position as Vector3).y < -0.45) if expect_authoritative_state: var quality: int = int(state.get("quality", -1)) assert(FishQuality.is_valid(quality)) @@ -196,7 +201,10 @@ func _validate_respawn_budget(service: Node) -> void: false, true, ) - assert((service.get("_entities") as Dictionary).size() == 6) + assert( + (service.get("_entities") as Dictionary).size() + == EXPECTED_POPULATION - crab_ids.size() + ) var now: float = Time.get_ticks_msec() / 1000.0 var respawns: Array = service.get("_respawns") @@ -209,18 +217,27 @@ func _validate_respawn_budget(service: Node) -> void: respawns[index] = respawn service.call("_update_respawns") - assert((service.get("_entities") as Dictionary).size() == 7) + assert( + (service.get("_entities") as Dictionary).size() + == EXPECTED_POPULATION - 1 + ) 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("_entities") as Dictionary).size() + == EXPECTED_POPULATION - 1 + ) 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("_entities") as Dictionary).size() + == EXPECTED_POPULATION + ) assert((service.get("_respawns") as Array).is_empty()) _validate_population(service, true) diff --git a/tests/world_spawn_protocol_validation.gd b/tests/world_spawn_protocol_validation.gd index 29f7fde..e300808 100644 --- a/tests/world_spawn_protocol_validation.gd +++ b/tests/world_spawn_protocol_validation.gd @@ -16,14 +16,18 @@ const CalendarSeasonType = preload("res://world/calendar_season.gd") func _initialize() -> void: - assert(NetworkProtocol.PROTOCOL_VERSION == 10) + 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() - _validate_billboard_presentation() + await _validate_billboard_presentation() _validate_envelopes() print("World spawn protocol validation: PASS") quit() @@ -51,6 +55,17 @@ func _validate_catalog_statuses() -> void: 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( + brown.surface_water_relation + == GatherableData.SurfaceWaterRelation.ABOVE_SALT_WATER + ) + assert(is_equal_approx( + brown.surface_population_per_100_square_meters, + 0.15, + )) + assert(brown.maximum_surface_population == 24) + assert(brown.target_population_for_surface_area(100.0) == 2) + assert(brown.target_population_for_surface_area(7500.0) == 12) 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") @@ -82,11 +97,27 @@ func _validate_catalog_statuses() -> void: 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 blue: GatherableData = Gatherables.get_entry(&"crab_blue") + assert(blue != null and blue.is_available()) + assert(blue.catch_data.active) + assert(blue.is_available(CalendarSeasonType.Season.SUMMER)) + assert(blue.is_available(CalendarSeasonType.Season.FALL)) + assert(not blue.is_available(CalendarSeasonType.Season.WINTER)) + assert( + blue.surface_water_relation + == GatherableData.SurfaceWaterRelation.BELOW_SALT_WATER + ) + assert(is_equal_approx( + blue.surface_population_per_100_square_meters, + 0.4, + )) + assert(blue.maximum_surface_population == 16) + assert(blue.target_population_for_surface_area(100.0) == 4) + assert(blue.target_population_for_surface_area(2250.0) == 9) + assert(FishCatalog.get_fish_by_id(&"crab_blue") == blue.catch_data) + assert(not blue.catch_data.is_fishable()) + + for type_id: StringName in [&"crab_ghost", &"crab_dungeness"]: var entry: GatherableData = Gatherables.get_entry(type_id) assert(entry != null) assert(entry.is_valid()) @@ -105,8 +136,9 @@ func _validate_catalog_statuses() -> void: ) var available: Array[GatherableData] = Gatherables.get_available_entries() - assert(available.size() == 3) + assert(available.size() == 4) assert(available.has(brown)) + assert(available.has(blue)) assert(available.has(clam)) assert(available.has(beetle)) var winter_available: Array[GatherableData] = ( @@ -114,6 +146,7 @@ func _validate_catalog_statuses() -> void: ) assert(winter_available.has(brown)) assert(winter_available.has(clam)) + assert(not winter_available.has(blue)) assert(not winter_available.has(beetle)) var rng := RandomNumberGenerator.new() rng.seed = 24680 @@ -148,8 +181,27 @@ func _validate_billboard_presentation() -> void: 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() - hotspot.call("_ensure_water_spurt_visual") + 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) @@ -158,6 +210,18 @@ func _validate_billboard_presentation() -> void: 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() diff --git a/tests/world_time_validation.gd b/tests/world_time_validation.gd index 17ddb49..3caea2b 100644 --- a/tests/world_time_validation.gd +++ b/tests/world_time_validation.gd @@ -304,7 +304,7 @@ func _validate_environment_presentation() -> void: var sky := Sky.new() var sky_material := ShaderMaterial.new() sky_material.shader = preload( - "res://world/environment/netfishing_sky.gdshader" + "res://world/environment/straywild_sky.gdshader" ) sky.sky_material = sky_material environment.sky = sky @@ -352,7 +352,7 @@ func _validate_environment_presentation() -> void: "res://world/materials/stylized_water.gdshader" ) var sky_shader: Shader = preload( - "res://world/environment/netfishing_sky.gdshader" + "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) diff --git a/tests/world_weather_validation.gd b/tests/world_weather_validation.gd index 1f46adf..7bc5114 100644 --- a/tests/world_weather_validation.gd +++ b/tests/world_weather_validation.gd @@ -283,7 +283,7 @@ func _validate_weather_presentation() -> void: var sky := Sky.new() var sky_material := ShaderMaterial.new() sky_material.shader = preload( - "res://world/environment/netfishing_sky.gdshader" + "res://world/environment/straywild_sky.gdshader" ) sky.sky_material = sky_material environment.sky = sky diff --git a/tools/blender/export_terrain_chunks.py b/tools/blender/export_terrain_chunks.py index ced081e..d4a278e 100644 --- a/tools/blender/export_terrain_chunks.py +++ b/tools/blender/export_terrain_chunks.py @@ -107,7 +107,7 @@ def _parse_arguments() -> argparse.Namespace: if "--" in sys.argv: script_arguments = sys.argv[sys.argv.index("--") + 1 :] parser = argparse.ArgumentParser( - description="Export NETfishing terrain chunk collections to GLB files." + description="Export straywild terrain chunk collections to GLB files." ) parser.add_argument( "--output", diff --git a/ui/assets/title/netfishing_logo.png b/ui/assets/title/netfishing_logo.png deleted file mode 100644 index 71c8ac8..0000000 Binary files a/ui/assets/title/netfishing_logo.png and /dev/null differ diff --git a/ui/assets/title/straywild_logo.png b/ui/assets/title/straywild_logo.png new file mode 100644 index 0000000..9f911f7 Binary files /dev/null and b/ui/assets/title/straywild_logo.png differ diff --git a/ui/assets/title/straywild_logo.png.import b/ui/assets/title/straywild_logo.png.import new file mode 100644 index 0000000..9cd04ca --- /dev/null +++ b/ui/assets/title/straywild_logo.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dwe5p5t0ode73" +path.s3tc="res://.godot/imported/straywild_logo.png-de66accac60dbd31604d2c39ab8d2775.s3tc.ctex" +path.etc2="res://.godot/imported/straywild_logo.png-de66accac60dbd31604d2c39ab8d2775.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://ui/assets/title/straywild_logo.png" +dest_files=["res://.godot/imported/straywild_logo.png-de66accac60dbd31604d2c39ab8d2775.s3tc.ctex", "res://.godot/imported/straywild_logo.png-de66accac60dbd31604d2c39ab8d2775.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/ui/components/bubble_menu/bubble_button.gd b/ui/components/bubble_menu/bubble_button.gd index 3dc3730..ac0409e 100644 --- a/ui/components/bubble_menu/bubble_button.gd +++ b/ui/components/bubble_menu/bubble_button.gd @@ -2,6 +2,13 @@ class_name BubbleButton extends Button const ICON_FILL_RATIO: float = 0.76 +const LABELED_ICON_FILL_RATIO: float = 0.58 +const BubbleGradientBackgroundType = preload( + "res://ui/components/bubble_menu/bubble_gradient_background.gd" +) +const BubbleHoverDrawerType = preload( + "res://ui/components/bubble_menu/bubble_hover_drawer.gd" +) @export var profile: BubbleMenuProfile @@ -16,6 +23,9 @@ const ICON_FILL_RATIO: float = 0.76 @export_range(1, 256, 1) var minimum_font_size: int = 14 @export_range(1, 256, 1) var maximum_font_size: int = 32 +@export_group("Icon") +@export_range(0.0, 1.0, 0.01) var icon_fill_ratio_override: float = 0.0 + @export_group("Motion") @export_range(0.0, 32.0, 0.1) var horizontal_amplitude: float = 1.8 @export_range(0.0, 32.0, 0.1) var vertical_amplitude: float = 4.0 @@ -23,20 +33,37 @@ const ICON_FILL_RATIO: float = 0.76 @export_range(0.0, TAU, 0.01) var motion_phase: float = 0.0 @export_range(0.0, 0.2, 0.001) var deformation_amplitude: float = 0.016 @export_range(0.1, 30.0, 0.1) var deformation_period: float = 6.0 +@export var hover_wiggle_amplitude: Vector2 = Vector2.ZERO +@export_range(0.1, 30.0, 0.1) var hover_wiggle_period: float = 2.0 + +@export_group("Hover Drawer") +@export var hover_drawer_text: String = "" +@export_enum("Right", "Left Vertical") var hover_drawer_mode: int = 0 +@export_range(0.1, 12.0, 0.1) var hover_drawer_speed: float = 4.0 + +@export_group("Hit Area") +@export var elliptical_hit_area: bool = true var neutral_position: Vector2 = Vector2.ZERO var presented_size: Vector2 = Vector2.ZERO var emphasis: float = 0.0 var _hovered: bool = false +var _drawer_focused: bool = false +var _drawer_reveal: float = 0.0 +var _gradient_background: BubbleGradientBackground +var _hover_drawer: BubbleHoverDrawer func _ready() -> void: mouse_entered.connect(_set_hovered.bind(true)) mouse_exited.connect(_set_hovered.bind(false)) + focus_entered.connect(_set_drawer_focused.bind(true)) + focus_exited.connect(_set_drawer_focused.bind(false)) resized.connect(_update_pivot) _update_pivot() apply_profile() _apply_icon_presentation(neutral_size) + _update_hover_drawer() func _gui_input(event: InputEvent) -> void: @@ -97,6 +124,8 @@ func apply_layout( presented_size = bubble_size _update_pivot() _apply_icon_presentation(bubble_size) + if _hover_drawer != null: + _hover_drawer.refresh_layout() var label_control: Control = get_label_control() var font_size := clampi( roundi(minf(bubble_size.x, bubble_size.y) * font_size_ratio), @@ -134,17 +163,26 @@ func _uses_icon_only_presentation() -> bool: func _apply_icon_presentation(bubble_size: Vector2) -> void: + if icon == null: + return + vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER + expand_icon = true + texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + var fill_ratio: float = ( + ICON_FILL_RATIO + if _uses_icon_only_presentation() + else LABELED_ICON_FILL_RATIO + ) + if _uses_icon_only_presentation() and icon_fill_ratio_override > 0.0: + fill_ratio = icon_fill_ratio_override + add_theme_constant_override( + "icon_max_width", + maxi(1, roundi(minf(bubble_size.x, bubble_size.y) * fill_ratio)), + ) if not _uses_icon_only_presentation(): return alignment = HORIZONTAL_ALIGNMENT_CENTER icon_alignment = HORIZONTAL_ALIGNMENT_CENTER - vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER - expand_icon = true - texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - add_theme_constant_override( - "icon_max_width", - maxi(1, roundi(minf(bubble_size.x, bubble_size.y) * ICON_FILL_RATIO)), - ) func advance_emphasis(delta: float) -> void: @@ -154,6 +192,27 @@ func advance_emphasis(delta: float) -> void: target, profile.emphasis_speed * delta ) + if _gradient_background != null: + _gradient_background.queue_redraw() + var drawer_target: float = 1.0 if (_hovered or _drawer_focused) else 0.0 + _drawer_reveal = move_toward( + _drawer_reveal, + drawer_target, + hover_drawer_speed * delta, + ) + if _hover_drawer != null: + _hover_drawer.set_reveal(_drawer_reveal) + + +func reset_hover_presentation() -> void: + _hovered = false + _drawer_focused = false + emphasis = 0.0 + _drawer_reveal = 0.0 + if _gradient_background != null: + _gradient_background.queue_redraw() + if _hover_drawer != null: + _hover_drawer.set_reveal(0.0) func calculate_target( @@ -169,12 +228,17 @@ func calculate_target( sin(phase * 0.73 + motion_phase) * horizontal_amplitude, sin(phase) * vertical_amplitude ) * layout_scale * motion_scale + var hover_phase: float = elapsed / hover_wiggle_period * TAU + var hover_offset := Vector2( + sin(hover_phase), + sin(hover_phase * 2.0 + motion_phase) * 0.5, + ) * hover_wiggle_amplitude * emphasis * layout_scale * motion_scale idle_offset.y -= ( profile.hover_focus_lift * emphasis * layout_scale ) - return neutral_position + idle_offset + return neutral_position + idle_offset + hover_offset func calculate_visual_scale( @@ -219,6 +283,8 @@ func apply_presentation( func _has_point(point: Vector2) -> bool: + if not elliptical_hit_area: + return Rect2(Vector2.ZERO, size).has_point(point) var radius: Vector2 = size * 0.5 if radius.x <= 0.0 or radius.y <= 0.0: return false @@ -228,6 +294,12 @@ func _has_point(point: Vector2) -> bool: func _set_hovered(value: bool) -> void: _hovered = value + if _gradient_background != null: + _gradient_background.queue_redraw() + + +func _set_drawer_focused(value: bool) -> void: + _drawer_focused = value func _update_pivot() -> void: @@ -237,6 +309,7 @@ func _update_pivot() -> void: func apply_profile() -> void: if profile == null: return + _update_gradient_background() add_theme_stylebox_override("normal", profile.make_normal_style()) var hover_style: StyleBoxFlat = profile.make_hover_style() add_theme_stylebox_override("hover", hover_style) @@ -251,3 +324,35 @@ func apply_profile() -> void: var label_control: Control = get_label_control() if label_control != self: label_control.add_theme_color_override("font_color", profile.text_color) + + +func _update_gradient_background() -> void: + if not profile.gradient_enabled: + if _gradient_background != null: + _gradient_background.queue_free() + _gradient_background = null + return + if _gradient_background == null: + _gradient_background = BubbleGradientBackgroundType.new() + _gradient_background.name = "GradientBackground" + add_child(_gradient_background) + move_child(_gradient_background, 0) + _gradient_background.configure(self, profile) + + +func _update_hover_drawer() -> void: + if hover_drawer_text.is_empty(): + if _hover_drawer != null: + _hover_drawer.queue_free() + _hover_drawer = null + return + if _hover_drawer == null: + _hover_drawer = BubbleHoverDrawerType.new() + _hover_drawer.name = "HoverDrawer" + add_child(_hover_drawer) + move_child(_hover_drawer, 0) + _hover_drawer.configure( + self, + hover_drawer_text, + hover_drawer_mode as BubbleHoverDrawer.DrawerMode, + ) diff --git a/ui/components/bubble_menu/bubble_cluster.gd b/ui/components/bubble_menu/bubble_cluster.gd index 0ad2624..29f7889 100644 --- a/ui/components/bubble_menu/bubble_cluster.gd +++ b/ui/components/bubble_menu/bubble_cluster.gd @@ -9,6 +9,7 @@ const ControllerFocusNavigationType = preload( @export var desktop_reference_size: Vector2 = Vector2(396.0, 318.0) @export var compact_reference_size: Vector2 = Vector2(294.0, 200.0) @export_range(0.0, 1.0, 0.01) var motion_scale: float = 1.0 +@export var collision_separation_enabled: bool = true var _bubbles: Array[BubbleButton] = [] var _elapsed: float = 0.0 @@ -70,39 +71,40 @@ func advance_motion(delta: float) -> void: targets.append(target) visual_scales.append(visual_scale) visual_radii.append(bubble.get_visual_radius(visual_scale)) - for first_index: int in _bubbles.size(): - for second_index: int in range(first_index + 1, _bubbles.size()): - var first_center: Vector2 = ( - targets[first_index] - + _bubbles[first_index].presented_size * 0.5 - ) - var second_center: Vector2 = ( - targets[second_index] - + _bubbles[second_index].presented_size * 0.5 - ) - var center_delta: Vector2 = second_center - first_center - var distance: float = center_delta.length() - var desired_distance: float = ( - visual_radii[first_index] - + visual_radii[second_index] - + profile.contact_gap * _layout_scale - ) - if distance >= desired_distance: - continue - var direction := ( - center_delta / distance - if distance > 0.001 - else Vector2.RIGHT.rotated(float(first_index + 1)) - ) - var correction: Vector2 = ( - direction - * minf( - (desired_distance - distance) * 0.5, - profile.maximum_separation * _layout_scale + if collision_separation_enabled: + for first_index: int in _bubbles.size(): + for second_index: int in range(first_index + 1, _bubbles.size()): + var first_center: Vector2 = ( + targets[first_index] + + _bubbles[first_index].presented_size * 0.5 ) - ) - targets[first_index] -= correction - targets[second_index] += correction + var second_center: Vector2 = ( + targets[second_index] + + _bubbles[second_index].presented_size * 0.5 + ) + var center_delta: Vector2 = second_center - first_center + var distance: float = center_delta.length() + var desired_distance: float = ( + visual_radii[first_index] + + visual_radii[second_index] + + profile.contact_gap * _layout_scale + ) + if distance >= desired_distance: + continue + var direction := ( + center_delta / distance + if distance > 0.001 + else Vector2.RIGHT.rotated(float(first_index + 1)) + ) + var correction: Vector2 = ( + direction + * minf( + (desired_distance - distance) * 0.5, + profile.maximum_separation * _layout_scale + ) + ) + targets[first_index] -= correction + targets[second_index] += correction var position_weight: float = 1.0 - exp( -profile.position_response * delta ) diff --git a/ui/components/bubble_menu/bubble_gradient_background.gd b/ui/components/bubble_menu/bubble_gradient_background.gd new file mode 100644 index 0000000..cf17bdb --- /dev/null +++ b/ui/components/bubble_menu/bubble_gradient_background.gd @@ -0,0 +1,79 @@ +class_name BubbleGradientBackground +extends Control + +const CORNER_SEGMENTS: int = 8 + +var host_button: BaseButton +var profile: BubbleMenuProfile + + +func configure( + button: BaseButton, + menu_profile: BubbleMenuProfile, +) -> void: + host_button = button + profile = menu_profile + mouse_filter = Control.MOUSE_FILTER_IGNORE + show_behind_parent = true + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + queue_redraw() + + +func _draw() -> void: + if host_button == null or profile == null or size.x <= 0.0 or size.y <= 0.0: + return + var fill_color: Color = _get_fill_color() + var top_color: Color = fill_color.lightened(profile.gradient_top_lift) + var points := PackedVector2Array() + _append_arc(points, Vector2(profile.corner_radius, profile.corner_radius), PI, PI * 1.5) + _append_arc( + points, + Vector2(size.x - profile.corner_radius, profile.corner_radius), + PI * 1.5, + TAU, + ) + _append_arc( + points, + Vector2(size.x - profile.corner_radius, size.y - profile.corner_radius), + 0.0, + PI * 0.5, + ) + _append_arc( + points, + Vector2(profile.corner_radius, size.y - profile.corner_radius), + PI * 0.5, + PI, + ) + var colors := PackedColorArray() + for point: Vector2 in points: + colors.append(top_color.lerp(fill_color, clampf(point.y / size.y, 0.0, 1.0))) + draw_polygon(points, colors) + + +func _append_arc( + points: PackedVector2Array, + center: Vector2, + start_angle: float, + end_angle: float, +) -> void: + var radius: float = minf( + float(profile.corner_radius), + minf(size.x, size.y) * 0.5, + ) + for index: int in range(CORNER_SEGMENTS + 1): + var weight: float = float(index) / float(CORNER_SEGMENTS) + var angle: float = lerpf(start_angle, end_angle, weight) + points.append(center + Vector2(cos(angle), sin(angle)) * radius) + + +func _get_fill_color() -> Color: + if host_button.disabled: + return profile.disabled_fill + match host_button.get_draw_mode(): + BaseButton.DRAW_PRESSED, BaseButton.DRAW_HOVER_PRESSED: + return profile.pressed_fill + BaseButton.DRAW_HOVER: + return profile.hover_fill + if host_button.has_focus(): + return profile.hover_fill + return profile.normal_fill diff --git a/ui/components/bubble_menu/bubble_gradient_background.gd.uid b/ui/components/bubble_menu/bubble_gradient_background.gd.uid new file mode 100644 index 0000000..ecab908 --- /dev/null +++ b/ui/components/bubble_menu/bubble_gradient_background.gd.uid @@ -0,0 +1 @@ +uid://cckk3lwdqadjn diff --git a/ui/components/bubble_menu/bubble_hover_drawer.gd b/ui/components/bubble_menu/bubble_hover_drawer.gd new file mode 100644 index 0000000..ad72edc --- /dev/null +++ b/ui/components/bubble_menu/bubble_hover_drawer.gd @@ -0,0 +1,97 @@ +class_name BubbleHoverDrawer +extends Control + +enum DrawerMode { + RIGHT, + LEFT_VERTICAL, +} + +const DRAWER_COLOR: Color = Color(0.031, 0.122, 0.169, 1.0) +const TEXT_COLOR: Color = Color(0.764706, 0.87451, 0.901961, 1.0) +const RIGHT_SIZE: Vector2 = Vector2(132.0, 48.0) +const LEFT_VERTICAL_SIZE: Vector2 = Vector2(50.0, 176.0) +const OVERLAP: float = 12.0 + +var host_button: BaseButton +var drawer_mode: DrawerMode = DrawerMode.RIGHT +var _panel: Panel +var _label: Label + + +func configure( + button: BaseButton, + drawer_text: String, + mode: DrawerMode, +) -> void: + host_button = button + drawer_mode = mode + mouse_filter = Control.MOUSE_FILTER_IGNORE + show_behind_parent = true + _build_controls() + _label.text = drawer_text + _refresh_label_layout() + refresh_layout() + set_reveal(0.0) + + +func refresh_layout() -> void: + if host_button == null: + return + if drawer_mode == DrawerMode.RIGHT: + size = RIGHT_SIZE + position = Vector2( + host_button.size.x - OVERLAP, + (host_button.size.y - size.y) * 0.5, + ) + pivot_offset = Vector2(0.0, size.y * 0.5) + else: + size = LEFT_VERTICAL_SIZE + position = Vector2( + -size.x + OVERLAP, + (host_button.size.y - size.y) * 0.5, + ) + pivot_offset = Vector2(size.x, size.y * 0.5) + _refresh_label_layout() + + +func set_reveal(amount: float) -> void: + var reveal: float = clampf(amount, 0.0, 1.0) + visible = reveal > 0.001 + scale = Vector2(maxf(reveal, 0.001), 1.0) + + +func _build_controls() -> void: + if _panel != null: + return + _panel = Panel.new() + _panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + _panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + var panel_style := StyleBoxFlat.new() + panel_style.bg_color = DRAWER_COLOR + panel_style.corner_radius_top_left = 12 + panel_style.corner_radius_top_right = 12 + panel_style.corner_radius_bottom_right = 12 + panel_style.corner_radius_bottom_left = 12 + _panel.add_theme_stylebox_override("panel", panel_style) + add_child(_panel) + + _label = Label.new() + _label.mouse_filter = Control.MOUSE_FILTER_IGNORE + _label.add_theme_color_override("font_color", TEXT_COLOR) + _label.add_theme_font_size_override("font_size", 20) + _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + add_child(_label) + + +func _refresh_label_layout() -> void: + if _label == null: + return + if drawer_mode == DrawerMode.RIGHT: + _label.rotation = 0.0 + _label.position = Vector2.ZERO + _label.size = size + else: + _label.rotation = -PI * 0.5 + _label.position = Vector2(-OVERLAP * 0.5, size.y) + _label.size = Vector2(size.y, size.x) diff --git a/ui/components/bubble_menu/bubble_hover_drawer.gd.uid b/ui/components/bubble_menu/bubble_hover_drawer.gd.uid new file mode 100644 index 0000000..9a31896 --- /dev/null +++ b/ui/components/bubble_menu/bubble_hover_drawer.gd.uid @@ -0,0 +1 @@ +uid://jvw68gltfda0 diff --git a/ui/components/bubble_menu/bubble_menu_profile.gd b/ui/components/bubble_menu/bubble_menu_profile.gd index a78cbd1..1af8b50 100644 --- a/ui/components/bubble_menu/bubble_menu_profile.gd +++ b/ui/components/bubble_menu/bubble_menu_profile.gd @@ -21,6 +21,10 @@ extends Resource @export_range(0, 12, 1) var normal_border_width: int = 2 @export_range(0, 12, 1) var emphasized_border_width: int = 3 +@export_group("Gradient") +@export var gradient_enabled: bool = false +@export_range(0.0, 0.5, 0.01) var gradient_top_lift: float = 0.12 + @export_group("Colors") @export var normal_fill: Color = Color(0.82, 0.93, 0.96, 0.96) @export var normal_border: Color = Color(0.91, 0.98, 1.0, 0.85) @@ -78,7 +82,11 @@ func _make_style( style.content_margin_top = content_margin style.content_margin_right = content_margin style.content_margin_bottom = content_margin - style.bg_color = fill_color + style.bg_color = ( + Color(fill_color.r, fill_color.g, fill_color.b, 0.0) + if gradient_enabled + else fill_color + ) style.border_width_left = outline_width style.border_width_top = outline_width style.border_width_right = outline_width diff --git a/ui/components/bubble_menu/title_menu_profile.tres b/ui/components/bubble_menu/title_menu_profile.tres new file mode 100644 index 0000000..3ae3a4a --- /dev/null +++ b/ui/components/bubble_menu/title_menu_profile.tres @@ -0,0 +1,22 @@ +[gd_resource type="Resource" script_class="BubbleMenuProfile" load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_menu_profile.gd" id="1_profile"] + +[resource] +script = ExtResource("1_profile") +font_size_ratio = 0.14 +hover_focus_scale = 1.02 +hover_focus_lift = 0.0 +emphasis_speed = 7.0 +emphasized_deformation_scale = 0.0 +contact_gap = 0.0 +maximum_separation = 0.0 +content_margin = 6.0 +corner_radius = 36 +normal_border_width = 0 +emphasized_border_width = 0 +gradient_enabled = false +normal_fill = Color(1, 1, 1, 1) +hover_fill = Color(1, 1, 1, 1) +pressed_fill = Color(1, 1, 1, 1) +disabled_fill = Color(1, 1, 1, 0.78) diff --git a/ui/components/bubble_menu/title_secondary_menu_profile.tres b/ui/components/bubble_menu/title_secondary_menu_profile.tres new file mode 100644 index 0000000..d79b7da --- /dev/null +++ b/ui/components/bubble_menu/title_secondary_menu_profile.tres @@ -0,0 +1,22 @@ +[gd_resource type="Resource" script_class="BubbleMenuProfile" load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_menu_profile.gd" id="1_profile"] + +[resource] +script = ExtResource("1_profile") +font_size_ratio = 0.14 +hover_focus_scale = 1.02 +hover_focus_lift = 0.0 +emphasis_speed = 7.0 +emphasized_deformation_scale = 0.0 +contact_gap = 0.0 +maximum_separation = 0.0 +content_margin = 6.0 +corner_radius = 16 +normal_border_width = 0 +emphasized_border_width = 0 +gradient_enabled = false +normal_fill = Color(1, 1, 1, 1) +hover_fill = Color(1, 1, 1, 1) +pressed_fill = Color(1, 1, 1, 1) +disabled_fill = Color(1, 1, 1, 0.78) diff --git a/ui/controller_focus_presentation.gd b/ui/controller_focus_presentation.gd index 405b834..cb41c31 100644 --- a/ui/controller_focus_presentation.gd +++ b/ui/controller_focus_presentation.gd @@ -245,7 +245,7 @@ func _clear_directional_input_in_flight(generation: int) -> void: func _is_directional_navigation(event: InputEvent) -> bool: if event is InputEventKey and (event as InputEventKey).echo: return false - # Tab is both Godot's default ui_focus_next key and NETfishing's default + # Tab is both Godot's default ui_focus_next key and straywild's default # player-menu binding. The global focus presenter must leave that gameplay # action alone; otherwise a remembered neutral focus seed consumes the key # before PlayerMenu can open or close. diff --git a/ui/controller_mapping_panel.gd b/ui/controller_mapping_panel.gd index 16080a7..12ac941 100644 --- a/ui/controller_mapping_panel.gd +++ b/ui/controller_mapping_panel.gd @@ -239,7 +239,7 @@ func _build_interface() -> void: _instruction_label = Label.new() _instruction_label.text = ( - "auto-map walks through NETfishing's controller actions. " + "auto-map walks through straywild's controller actions. " + "select any row afterward to override it; the mapped back " + "control cancels capture." ) diff --git a/ui/decor_shop.gd b/ui/decor_shop.gd new file mode 100644 index 0000000..d05d9eb --- /dev/null +++ b/ui/decor_shop.gd @@ -0,0 +1,331 @@ +class_name DecorShop +extends Control + +const INPUT_OWNER: StringName = &"decor_shop" +const DecorCatalogType = preload("res://homes/decor_catalog.gd") +const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") + +signal menu_visibility_changed(is_open: bool) +signal menu_exit_started + +var _player: Player +var _wallet: PlayerWallet +var _fishing_spot: FishingSpot +var _interaction: DecorShopInteraction +var _bag: PlayerBag +var _home_state: PlayerHomeState +var _network_shop: NetworkShopService +var _wallet_label: Label +var _owned_label: Label +var _feedback: Label +var _buy_button: Button +var _close_button: Button +var _transaction_pending: bool = false +var _prior_movement_enabled: bool = true +var _prior_camera_enabled: bool = true +var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE + + +func _ready() -> void: + UtilityPageStyleType.apply_page(self) + _build_ui() + hide() + + +func setup( + player: Player, + wallet: PlayerWallet, + fishing_spot: FishingSpot, + interaction: DecorShopInteraction, + bag: PlayerBag, + home_state: PlayerHomeState, + network_shop: NetworkShopService, +) -> void: + _player = player + _wallet = wallet + _fishing_spot = fishing_spot + _interaction = interaction + _bag = bag + _home_state = home_state + _network_shop = network_shop + if not _wallet.balance_changed.is_connected(_on_wallet_changed): + _wallet.balance_changed.connect(_on_wallet_changed) + if not _bag.contents_changed.is_connected(_refresh): + _bag.contents_changed.connect(_refresh) + if not _home_state.changed.is_connected(_refresh): + _home_state.changed.connect(_refresh) + if ( + _network_shop != null + and not _network_shop.local_purchase_pending.is_connected( + _on_purchase_pending + ) + ): + _network_shop.local_purchase_pending.connect(_on_purchase_pending) + _network_shop.local_purchase_finished.connect(_on_purchase_finished) + _refresh() + + +func set_world_interaction(interaction: DecorShopInteraction) -> void: + _interaction = interaction + + +func open_shop() -> bool: + if ( + visible + or _player == null + or _wallet == null + or _fishing_spot == null + or _interaction == null + or not _interaction.is_local_player_in_range() + or not _fishing_spot.can_open_fishing_shop() + ): + return false + _prior_movement_enabled = _player.is_movement_enabled() + _prior_camera_enabled = _player.is_camera_input_enabled() + _player.set_movement_enabled(false) + # Release any active right-click drag first. The shop must not remember the + # drag's transient captured mode because release may happen inside the UI. + _player.set_camera_input_enabled(false) + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) + _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _transaction_pending = ( + _network_shop != null + and _network_shop.is_local_purchase_pending() + ) + _feedback.text = "" + _refresh() + show() + _buy_button.call_deferred("grab_focus") + menu_visibility_changed.emit(true) + return true + + +func close_shop(restore_controls: bool = true) -> void: + if not visible: + return + menu_exit_started.emit() + get_viewport().gui_release_focus() + hide() + if _fishing_spot != null: + _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false) + if restore_controls and _player != null: + _player.set_movement_enabled(_prior_movement_enabled) + _player.set_camera_input_enabled(_prior_camera_enabled) + Input.mouse_mode = _prior_mouse_mode + menu_visibility_changed.emit(false) + + +func consume_escape() -> bool: + if not visible: + return false + close_shop() + return true + + +func close_for_range_exit() -> void: + close_shop() + + +func close_for_water_recovery() -> void: + close_shop(false) + + +func close_for_session_end() -> void: + close_shop(false) + + +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 _unhandled_input(event: InputEvent) -> void: + if not visible or not event.is_action_pressed("ui_cancel"): + return + close_shop() + get_viewport().set_input_as_handled() + + +func _purchase_cube() -> void: + if _network_shop == null or _transaction_pending: + return + var request_id: String = _network_shop.request_decor( + DecorCatalogType.BASIC_CUBE_ID + ) + if request_id.is_empty(): + _refresh() + + +func _on_purchase_pending(_request_id: String) -> void: + _transaction_pending = true + _feedback.text = "purchasing…" + _refresh() + + +func _on_purchase_finished( + _request_id: String, + accepted: bool, + message: String, + product_id: StringName, + category: int, + _quantity: int, + total_cost: int, +) -> void: + if ( + category != NetworkShopProtocol.ProductCategory.DECOR + or product_id != DecorCatalogType.BASIC_CUBE_ID + ): + return + _transaction_pending = false + _feedback.text = ( + "basic cube purchased · %d fishcoin spent" % total_cost + if accepted + else message.to_lower() + ) + _refresh() + + +func _on_wallet_changed(_balance: int, _delta: int) -> void: + _refresh() + + +func _refresh() -> void: + if _wallet_label == null: + return + var balance: int = _wallet.get_balance() if _wallet != null else 0 + var owned: int = ( + _home_state.get_owned_count(DecorCatalogType.BASIC_CUBE_ID) + if _home_state != null else 0 + ) + var carried: int = ( + _bag.get_quantity(DecorCatalogType.BASIC_CUBE_ID) + if _bag != null else 0 + ) + _wallet_label.text = "%d fishcoins" % balance + _owned_label.text = "owned %d · packed %d" % [owned, carried] + _buy_button.disabled = ( + _transaction_pending + or _wallet == null + or not _wallet.can_afford( + DecorCatalogType.get_price(DecorCatalogType.BASIC_CUBE_ID) + ) + or _bag == null + or not _bag.can_add_item(DecorCatalogType.BASIC_CUBE_ID, 1) + or owned >= DecorCatalogType.MAX_OWNED_PER_PRODUCT + ) + + +func _build_ui() -> void: + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + z_index = 83 + mouse_filter = Control.MOUSE_FILTER_STOP + var blocker := ColorRect.new() + blocker.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + blocker.color = Color(0.02, 0.08, 0.10, 0.55) + blocker.mouse_filter = Control.MOUSE_FILTER_STOP + add_child(blocker) + + var panel := PanelContainer.new() + panel.set_anchors_preset(Control.PRESET_CENTER) + panel.position = Vector2(-440.0, -250.0) + panel.size = Vector2(880.0, 500.0) + panel.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style( + UtilityPageStyleType.OCEAN_PANEL_DEEP, 24 + ), + ) + add_child(panel) + var margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + margin.add_theme_constant_override(side, 28) + panel.add_child(margin) + var layout := VBoxContainer.new() + layout.add_theme_constant_override("separation", 18) + margin.add_child(layout) + + var header := HBoxContainer.new() + header.add_theme_constant_override("separation", 12) + layout.add_child(header) + var title := Label.new() + title.text = "decor" + title.add_theme_font_size_override("font_size", 38) + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header.add_child(title) + _wallet_label = Label.new() + _wallet_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + _wallet_label.add_theme_font_size_override("font_size", 22) + header.add_child(_wallet_label) + + var item_panel := PanelContainer.new() + item_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL + item_panel.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style( + UtilityPageStyleType.OCEAN_FIELD, 18 + ), + ) + layout.add_child(item_panel) + var item_margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + item_margin.add_theme_constant_override(side, 24) + item_panel.add_child(item_margin) + var item_row := HBoxContainer.new() + item_row.add_theme_constant_override("separation", 28) + item_margin.add_child(item_row) + var preview := Panel.new() + preview.custom_minimum_size = Vector2(150.0, 150.0) + preview.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style(Color("b77a55"), 12), + ) + item_row.add_child(preview) + var details := VBoxContainer.new() + details.size_flags_horizontal = Control.SIZE_EXPAND_FILL + details.add_theme_constant_override("separation", 10) + item_row.add_child(details) + var item_name := Label.new() + item_name.text = DecorCatalogType.get_display_name( + DecorCatalogType.BASIC_CUBE_ID + ) + item_name.add_theme_font_size_override("font_size", 32) + details.add_child(item_name) + var description := Label.new() + description.text = DecorCatalogType.get_description( + DecorCatalogType.BASIC_CUBE_ID + ) + description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + description.add_theme_font_size_override("font_size", 20) + description.size_flags_vertical = Control.SIZE_EXPAND_FILL + details.add_child(description) + _owned_label = Label.new() + _owned_label.add_theme_font_size_override("font_size", 18) + details.add_child(_owned_label) + _buy_button = Button.new() + _buy_button.text = "buy · 1 fishcoin" + _buy_button.custom_minimum_size = Vector2(260.0, 58.0) + UtilityPageStyleType.apply_ocean_button(_buy_button) + _buy_button.pressed.connect(_purchase_cube) + details.add_child(_buy_button) + + var footer := HBoxContainer.new() + footer.add_theme_constant_override("separation", 12) + layout.add_child(footer) + _feedback = Label.new() + _feedback.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _feedback.add_theme_font_size_override("font_size", 18) + footer.add_child(_feedback) + _close_button = Button.new() + _close_button.text = "close" + _close_button.custom_minimum_size = Vector2(150.0, 50.0) + UtilityPageStyleType.apply_ocean_button(_close_button) + _close_button.pressed.connect(close_shop) + footer.add_child(_close_button) + _buy_button.focus_neighbor_bottom = _buy_button.get_path_to(_close_button) + _close_button.focus_neighbor_top = _close_button.get_path_to(_buy_button) diff --git a/ui/decor_shop.gd.uid b/ui/decor_shop.gd.uid new file mode 100644 index 0000000..ed65203 --- /dev/null +++ b/ui/decor_shop.gd.uid @@ -0,0 +1 @@ +uid://810ck3mvbn5v diff --git a/ui/decor_shop.tscn b/ui/decor_shop.tscn new file mode 100644 index 0000000..2b26f04 --- /dev/null +++ b/ui/decor_shop.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/decor_shop.gd" id="1_script"] + +[node name="DecorShop" type="Control"] +unique_name_in_owner = true +visible = false +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_script") diff --git a/ui/fishing_danger_vignette.gdshader b/ui/fishing_danger_vignette.gdshader new file mode 100644 index 0000000..8eee3df --- /dev/null +++ b/ui/fishing_danger_vignette.gdshader @@ -0,0 +1,10 @@ +shader_type canvas_item; + +uniform float intensity : hint_range(0.0, 1.0) = 0.0; + +void fragment() { + vec2 centered = UV * 2.0 - 1.0; + centered.x *= 0.56; + float edge = smoothstep(0.42, 1.02, length(centered)); + COLOR = vec4(0.16, 0.018, 0.025, edge * intensity * 0.72); +} diff --git a/ui/fishing_danger_vignette.gdshader.uid b/ui/fishing_danger_vignette.gdshader.uid new file mode 100644 index 0000000..578ec8b --- /dev/null +++ b/ui/fishing_danger_vignette.gdshader.uid @@ -0,0 +1 @@ +uid://c1qcj3m4b10x4 diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index ad8247c..7cc8fa8 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -15,9 +15,6 @@ const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" ) const PlayerType = preload("res://player/player.gd") -const PlayerCoolerCapacityType = preload( - "res://progression/player_cooler_capacity.gd" -) const PlayerArtUnlocksType = preload( "res://progression/player_art_unlocks.gd" ) @@ -154,9 +151,6 @@ const ROD_PRICE_ICON_SIZE: float = 22.0 @onready var _barrier_price_bubble: PanelContainer = %BarrierPriceBubble @onready var _barrier_purchase: Button = %BarrierPurchase @onready var _supplies_list: VBoxContainer = %SuppliesList -@onready var _cooler_cost: CurrencyAmount = %CoolerCost -@onready var _cooler_price_bubble: PanelContainer = %CoolerPriceBubble -@onready var _cooler_purchase: Button = %CoolerPurchase @onready var _backpack_cost: CurrencyAmount = %BackpackCost @onready var _backpack_price_bubble: PanelContainer = %BackpackPriceBubble @onready var _backpack_purchase: Button = %BackpackPurchase @@ -172,7 +166,6 @@ var _inventory: FishInventory var _hotbar: PlayerHotbar var _inventory_layout: PlayerInventoryLayout var _item_catalog: ItemCatalogType -var _cooler_capacity: PlayerCoolerCapacityType var _art_unlocks: PlayerArtUnlocksType var _network_shop: NetworkShopService var _network_sale: NetworkSaleService @@ -202,7 +195,6 @@ func _ready() -> void: %CloseButton.pressed.connect(close_shop) _reel_purchase.pressed.connect(_purchase_reel_speed) _barrier_purchase.pressed.connect(_purchase_barrier_power) - _cooler_purchase.pressed.connect(_purchase_cooler_capacity) _backpack_purchase.pressed.connect(_purchase_backpack_capacity) @@ -353,7 +345,6 @@ func _focus_shop_section() -> void: for upgrade_button: Button in [ _reel_purchase, _barrier_purchase, - _cooler_purchase, _backpack_purchase, ]: if not upgrade_button.disabled: @@ -509,7 +500,6 @@ func _apply_shop_styles() -> void: for panel: PanelContainer in [ _reel_price_bubble, _barrier_price_bubble, - _cooler_price_bubble, _backpack_price_bubble, ]: var price_style := UtilityPageStyleType.rounded_style( @@ -526,7 +516,6 @@ func _apply_shop_styles() -> void: for price: CurrencyAmount in [ _reel_cost, _barrier_cost, - _cooler_cost, _backpack_cost, ]: var amount_label := price.get_amount_label() @@ -538,7 +527,6 @@ func _apply_shop_styles() -> void: %CloseButton, _reel_purchase, _barrier_purchase, - _cooler_purchase, _backpack_purchase, ]: UtilityPageStyleType.apply_ocean_button(button) @@ -559,7 +547,6 @@ func setup( hotbar: PlayerHotbar, inventory_layout: PlayerInventoryLayout, item_catalog: ItemCatalogType, - cooler_capacity: PlayerCoolerCapacityType, art_unlocks: PlayerArtUnlocksType, network_shop: NetworkShopService, network_sale: NetworkSaleService, @@ -576,7 +563,6 @@ func setup( _hotbar = hotbar _inventory_layout = inventory_layout _item_catalog = item_catalog - _cooler_capacity = cooler_capacity _art_unlocks = art_unlocks _network_shop = network_shop _network_sale = network_sale @@ -612,10 +598,6 @@ func setup( _upgrades.upgrades_changed.connect(_on_upgrades_changed) if not _bag.contents_changed.is_connected(_on_bag_changed): _bag.contents_changed.connect(_on_bag_changed) - if not _cooler_capacity.capacity_changed.is_connected( - _on_cooler_capacity_changed - ): - _cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed) if not _inventory_layout.backpack_capacity_changed.is_connected( _on_backpack_capacity_changed ): @@ -654,11 +636,14 @@ func open_shop() -> bool: ) _prior_movement_enabled = _player.is_movement_enabled() _prior_camera_enabled = _player.is_camera_input_enabled() - _prior_mouse_mode = Input.mouse_mode + _player.set_movement_enabled(false) + # Releasing an active camera drag before the snapshot prevents the close + # path from restoring a captured pointer after its matching button release + # has been consumed by the shop. + _player.set_camera_input_enabled(false) + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) _snapshot_stored = true _mouse_snapshot_stored = true - _player.set_movement_enabled(false) - _player.set_camera_input_enabled(false) _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _set_feedback("") @@ -833,7 +818,6 @@ func _refresh_all() -> void: _refresh_wallet() _refresh_upgrades() _refresh_supplies() - _refresh_cooler_capacity() _refresh_backpack_capacity() @@ -1584,41 +1568,6 @@ func _add_supply_icon_tile( return tile_host -func _refresh_cooler_capacity() -> void: - if _cooler_capacity == null: - return - var level: int = _cooler_capacity.get_level() - var cost: int = _cooler_capacity.get_next_cost() - var next_capacity: int = _cooler_capacity.get_next_capacity() - _cooler_purchase.disabled = ( - cost < 0 - or _transaction_in_progress - or _closing - or _network_shop == null - or not _network_shop.can_request_purchase() - or not _cooler_capacity.can_purchase(_wallet) - ) - var cooler_effect: String - if cost < 0: - cooler_effect = "%d slots · maximum level" % ( - _cooler_capacity.get_capacity() - ) - _cooler_price_bubble.hide() - else: - _cooler_price_bubble.show() - cooler_effect = "%d → %d slots" % [ - _cooler_capacity.get_capacity(), - next_capacity, - ] - _cooler_cost.set_amount(cost) - _cooler_purchase.tooltip_text = _upgrade_tooltip( - "storage capacity", - level, - cooler_effect, - ) - _cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text - - func _refresh_backpack_capacity() -> void: if _inventory_layout == null: return @@ -1720,23 +1669,6 @@ func _purchase_art_upgrade(product_id: StringName) -> void: _network_shop.request_art_upgrade(product_id) -func _purchase_cooler_capacity() -> void: - if _transaction_in_progress or not _is_transaction_context_valid(): - _set_feedback("unable to complete purchase.") - return - var cost: int = _cooler_capacity.get_next_cost() - if cost < 0: - _set_feedback("Upgrade is already at maximum.") - return - if not _wallet.can_afford(cost): - _set_feedback("Insufficient funds.") - return - if _network_shop == null: - _set_feedback("Purchase could not be completed.") - return - _network_shop.request_cooler_capacity_upgrade() - - func _purchase_backpack_capacity() -> void: if _transaction_in_progress or not _is_transaction_context_valid(): _set_feedback("unable to complete purchase.") @@ -1857,7 +1789,6 @@ func _on_wallet_changed(_balance: int, _delta: int) -> void: _refresh_wallet() _refresh_upgrades() _refresh_supplies() - _refresh_cooler_capacity() _refresh_backpack_capacity() @@ -1872,10 +1803,6 @@ func _on_bag_changed() -> void: _refresh_supplies() -func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void: - _refresh_cooler_capacity() - - func _on_backpack_capacity_changed(_level: int, _capacity: int) -> void: _refresh_backpack_capacity() @@ -1897,3 +1824,9 @@ func _apply_mouse_close_policy(reason: CloseReason) -> void: CloseReason.TEARDOWN: pass _mouse_snapshot_stored = false + + +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 diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 70ccec1..66fcbd9 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -257,35 +257,6 @@ layout_mode = 2 amount = 100 icon_size = 24.0 -[node name="CoolerTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"] -custom_minimum_size = Vector2(144, 168) -layout_mode = 2 -mouse_filter = 2 - -[node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"] -unique_name_in_owner = true -offset_right = 144.0 -offset_bottom = 144.0 -tooltip_text = "storage capacity" -icon = ExtResource("7_close") -expand_icon = true -icon_max_width = 92 - -[node name="CoolerPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"] -unique_name_in_owner = true -z_index = 2 -offset_left = 22.0 -offset_top = 128.0 -offset_right = 122.0 -offset_bottom = 160.0 -mouse_filter = 2 - -[node name="CoolerCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile/CoolerPriceBubble" instance=ExtResource("8_currency")] -unique_name_in_owner = true -layout_mode = 2 -amount = 75 -icon_size = 24.0 - [node name="BackpackTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"] custom_minimum_size = Vector2(144, 168) layout_mode = 2 diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 6efea59..55a07eb 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -20,6 +20,8 @@ const TitleScreenType = preload("res://ui/title_screen.gd") const PauseMenuType = preload("res://ui/pause_menu.gd") const NetworkSessionType = preload("res://network/network_session.gd") const FishingShopType = preload("res://ui/fishing_shop.gd") +const DecorShopType = preload("res://ui/decor_shop.gd") +const RVUpgradeShopType = preload("res://ui/rv_upgrade_shop.gd") const SettingsPanelType = preload("res://ui/settings_panel.gd") const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" @@ -27,7 +29,14 @@ const PlayerFishingUpgradesType = preload( const ShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const PlayerStorageType = preload("res://ui/player_storage.gd") +const HomeDecorToolbarType = preload("res://ui/home_decor_toolbar.gd") const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) @@ -71,6 +80,9 @@ const MAIN_BUBBLE_PROFILE: BubbleMenuProfile = preload( const ACTIVE_BAIT_EMPTY_ICON: Texture2D = preload( "res://ui/icons/pictograms/x_dark.png" ) +const RV_STORAGE_ICON: Texture2D = preload( + "res://ui/icons/tab_menu/inventory_simple.png" +) signal pixelation_settings_visibility_changed(is_visible: bool) signal crisp_reset_focus_requested @@ -81,6 +93,8 @@ signal shop_backdrop_visibility_changed(is_visible: bool) signal virtual_pointer_mode_changed(is_active: bool) signal social_prompt_accepted signal social_prompt_declined +signal data_root_changed +signal rv_storage_requested const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse" const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu" @@ -119,6 +133,7 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 @onready var _barrier_health: Label = %BarrierHealth @onready var _showcase_details: Label = %ShowcaseDetails @onready var _fishing_panel: PanelContainer = %FishingPanel +@onready var _fishing_danger_vignette: ColorRect = %FishingDangerVignette @onready var _experience_panel: PanelContainer = %ExperienceProgressPanel @onready var _experience_level_label: Label = %ExperienceLevelLabel @onready var _experience_award_label: Label = %ExperienceAwardLabel @@ -134,6 +149,8 @@ const SHOP_ANIMALESE_VOICE_ID: String = "natural" const SHOP_ANIMALESE_BASE_PITCH: float = 1.08 const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0 const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000 +const FISHING_DANGER_START_GAP: float = 0.15 +const FISHING_DANGER_FULL_GAP: float = 0.025 @onready var _canonical_stage: Control = %CanonicalStage @@ -144,7 +161,11 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000 @onready var _social_prompt: BubbleConfirmationPage = %SocialPrompt @onready var _hotbar_ui: HotbarUIType = %Hotbar @onready var _fishing_shop: FishingShopType = %FishingShop +@onready var _decor_shop: DecorShopType = %DecorShop +@onready var _rv_upgrade_shop: RVUpgradeShopType = %RVUpgradeShop @onready var _player_storage: PlayerStorageType = %PlayerStorage +@onready var _home_decor_toolbar: HomeDecorToolbarType = %HomeDecorToolbar +@onready var _rv_storage_button: Button = %RVStorageButton @onready var _storage_prompt: PanelContainer = %StoragePrompt @onready var _storage_prompt_message: Label = %StoragePromptMessage @onready var _shop_prompt: Control = %ShopPrompt @@ -194,6 +215,8 @@ var _chat_input_open: bool = false var _player_menu_hotbar_visible: bool = false var _main_shop_buyer: FishBuyerProfileType var _shop_interaction: ShopInteractionType +var _decor_shop_interaction: DecorShopInteractionType +var _rv_upgrade_shop_interaction: RVUpgradeInteractionType var _storage_interaction: PlayerStorageInteractionType var _surface_drawing: NetworkSurfaceDrawingService var _surface_drawing_hotbar_selected: bool = false @@ -220,14 +243,23 @@ var _controller_text_entry_request: Callable var _controller_text_entry_is_open: Callable var _controller_text_entry_close: Callable var _restore_chat_keyboard_after_fishing: bool = false +var _rv_storage_button_requested_visible: bool = false +var _previous_barrier_health := PackedInt32Array() +var _barrier_feedback_initialized: bool = false +var _fishing_danger_target: float = 0.0 +var _fishing_danger_intensity: float = 0.0 +var _fishing_danger_pulse_time: float = 0.0 +var _catch_track_shake_tween: Tween func _ready() -> void: _prioritize_surface_drawing_pointer_input() _bite_prompt_button.pressed.connect(_on_bite_prompt_pressed) + _rv_storage_button.pressed.connect(_on_rv_storage_button_pressed) _social_prompt.confirmed.connect(_accept_social_prompt) _social_prompt.cancelled.connect(_decline_social_prompt) _apply_active_bait_indicator_style() + _apply_rv_storage_button_style() _refresh_active_bait_indicator() _refresh_interaction_prompt_bindings() # Reward feedback must remain above full-screen canonical menus. Keeping the @@ -259,6 +291,9 @@ func _ready() -> void: _player_menu.controller_hotbar_management_ended.connect( _on_controller_hotbar_management_ended ) + _player_menu.controller_text_entry_requested.connect( + request_controller_text_entry_for + ) _title_settings_panel.panel_visibility_changed.connect( _on_settings_visibility_changed ) @@ -271,6 +306,8 @@ func _ready() -> void: _pause_settings_panel.crisp_reset_focus_requested.connect( crisp_reset_focus_requested.emit ) + _title_settings_panel.data_root_changed.connect(data_root_changed.emit) + _pause_settings_panel.data_root_changed.connect(data_root_changed.emit) if not Input.joy_connection_changed.is_connected( _on_controller_connection_changed ): @@ -340,11 +377,14 @@ func setup( fishing_upgrades: PlayerFishingUpgradesType, shop_interaction: ShopInteractionType, storage_interaction: PlayerStorageInteractionType, + decor_shop_interaction: DecorShopInteractionType, + rv_upgrade_shop_interaction: RVUpgradeInteractionType, item_effects: PlayerItemEffectsType, cooler_capacity: PlayerCoolerCapacityType, network_session: NetworkSessionType, network_sale_service: NetworkSaleService, network_shop_service: NetworkShopService, + home_state: PlayerHomeState, network_chat_service: NetworkChatService, network_profile: NetworkProfilePreferences, spawn_service: PlayerSpawnService, @@ -456,6 +496,7 @@ func setup( discovery, player_jobs, world_time, + world_weather, world_environment, world_sun, ) @@ -481,12 +522,28 @@ func setup( hotbar, inventory_layout, item_catalog, - cooler_capacity, art_unlocks, network_shop_service, network_sale_service, reservations, ) + _decor_shop.setup( + player, + wallet, + fishing_spot, + decor_shop_interaction, + bag, + home_state, + network_shop_service, + ) + _rv_upgrade_shop.setup( + player, + wallet, + fishing_spot, + rv_upgrade_shop_interaction, + home_state, + network_shop_service, + ) _player_storage.setup( player, fishing_spot, @@ -502,6 +559,10 @@ func setup( ) _fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) _fishing_shop.menu_exit_started.connect(_on_shop_exit_started) + _decor_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) + _decor_shop.menu_exit_started.connect(_on_shop_exit_started) + _rv_upgrade_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) + _rv_upgrade_shop.menu_exit_started.connect(_on_shop_exit_started) _fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested) _fishing_shop.shop_cooler_return_requested.connect( _on_shop_cooler_return_requested @@ -511,6 +572,8 @@ func setup( ) _main_shop_buyer = main_shop_buyer _shop_interaction = shop_interaction + _decor_shop_interaction = decor_shop_interaction + _rv_upgrade_shop_interaction = rv_upgrade_shop_interaction _storage_interaction = storage_interaction _surface_drawing = surface_drawing if ( @@ -533,11 +596,17 @@ func setup( func set_world_interactions( shop_interaction: ShopInteractionType, storage_interaction: PlayerStorageInteractionType, + decor_shop_interaction: DecorShopInteractionType, + rv_upgrade_shop_interaction: RVUpgradeInteractionType, ) -> void: _shop_interaction = shop_interaction _storage_interaction = storage_interaction + _decor_shop_interaction = decor_shop_interaction + _rv_upgrade_shop_interaction = rv_upgrade_shop_interaction _fishing_shop.set_world_interaction(shop_interaction) _player_storage.set_world_interaction(storage_interaction) + _decor_shop.set_world_interaction(decor_shop_interaction) + _rv_upgrade_shop.set_world_interaction(rv_upgrade_shop_interaction) func _prioritize_surface_drawing_pointer_input() -> void: @@ -1499,6 +1568,13 @@ func _process(delta: float) -> void: _update_virtual_mouse(delta) _update_controller_menu_scroll(delta) _update_experience_bubble_position() + _update_fishing_danger_vignette(delta) + _try_start_shop_npc_speech() + if _player != null: + _player.set_fishing_reel_camera_active( + _fishing_spot != null + and _fishing_spot.is_reel_input_active() + ) func _update_controller_menu_scroll(delta: float) -> void: @@ -1648,6 +1724,34 @@ func _apply_active_bait_indicator_style() -> void: _active_bait_button.add_theme_color_override(state, Color.WHITE) +func _apply_rv_storage_button_style() -> void: + var normal_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_normal_style() + var hover_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_hover_style() + var pressed_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_pressed_style() + for style: StyleBoxFlat in [normal_style, hover_style, pressed_style]: + style.set_corner_radius_all(29) + style.content_margin_left = 9.0 + style.content_margin_top = 9.0 + style.content_margin_right = 9.0 + style.content_margin_bottom = 9.0 + _rv_storage_button.icon = RV_STORAGE_ICON + _rv_storage_button.add_theme_stylebox_override("normal", normal_style) + _rv_storage_button.add_theme_stylebox_override("hover", hover_style) + _rv_storage_button.add_theme_stylebox_override("focus", hover_style) + _rv_storage_button.add_theme_stylebox_override("pressed", pressed_style) + for state: StringName in [ + &"icon_normal_color", + &"icon_hover_color", + &"icon_focus_color", + &"icon_pressed_color", + ]: + _rv_storage_button.add_theme_color_override(state, Color.WHITE) + + +func _on_rv_storage_button_pressed() -> void: + rv_storage_requested.emit() + + func _refresh_active_bait_indicator() -> void: _refresh_active_bait_indicator_visibility() if not is_node_ready(): @@ -1711,17 +1815,21 @@ func set_gameplay_ui_enabled(enabled: bool) -> void: if not enabled: _gameplay_hud_hidden = false _refresh_active_bait_indicator_visibility() + _refresh_rv_storage_button_visibility() if enabled: _try_start_shop_npc_speech() _refresh_gameplay_hud_visibility() _refresh_chat_availability() if not enabled: + _home_decor_toolbar.set_active(false) _end_virtual_mouse() _close_radial_menus() if _surface_drawing != null: _surface_drawing.deactivate() close_player_menu_for_session_end() _fishing_shop.close_for_session_end() + _decor_shop.close_for_session_end() + _rv_upgrade_shop.close_for_session_end() _player_storage.close_for_session_end() _fishing_panel.visible = false _shop_prompt.hide() @@ -1742,6 +1850,7 @@ func set_gameplay_hud_hidden(hidden: bool) -> void: _quick_radial_menu.set_hud_hidden(hidden) _refresh_gameplay_hud_visibility() _refresh_active_bait_indicator_visibility() + _refresh_rv_storage_button_visibility() _refresh_hotbar_visibility() _refresh_chat_availability() @@ -1857,13 +1966,47 @@ func get_fishing_shop() -> FishingShopType: return _fishing_shop +func get_decor_shop() -> DecorShopType: + return _decor_shop + + +func get_rv_upgrade_shop() -> RVUpgradeShopType: + return _rv_upgrade_shop + + func get_player_storage() -> PlayerStorageType: return _player_storage +func get_home_decor_toolbar() -> HomeDecorToolbarType: + return _home_decor_toolbar + + +func set_rv_interior_camera_mode(enabled: bool) -> void: + _hotbar_ui.set_rv_interior_camera_mode(enabled) + + +func set_rv_storage_button_visible(requested_visible: bool) -> void: + _rv_storage_button_requested_visible = requested_visible + _refresh_rv_storage_button_visibility() + + +func _refresh_rv_storage_button_visibility() -> void: + _rv_storage_button.visible = ( + _rv_storage_button_requested_visible + and _gameplay_ui_enabled + and not _gameplay_hud_hidden + and not _system_menu_open + and not _player_menu_open + and not _shop_open + and not _storage_open + ) + + func set_storage_prompt_visible( requested_visible: bool, world_anchor: Vector3 = Vector3(0.0, INF, 0.0), + action_text: String = "open storage", ) -> void: if not _storage_prompt.has_meta(&"styled"): _storage_prompt.set_meta(&"styled", true) @@ -1872,6 +2015,7 @@ func set_storage_prompt_visible( ) style.anti_aliasing = false _storage_prompt.add_theme_stylebox_override("panel", style) + _storage_prompt_message.text = action_text _storage_prompt.visible = ( requested_visible and _gameplay_ui_enabled @@ -1889,7 +2033,7 @@ func _position_storage_prompt(world_anchor: Vector3) -> void: if _player == null: _storage_prompt.hide() return - var camera := _player.get_gameplay_camera() + var camera := _player.get_active_gameplay_camera() if camera == null or camera.is_position_behind(world_anchor): _storage_prompt.hide() return @@ -1924,8 +2068,19 @@ func set_shop_prompt_visible( _position_shop_prompt(world_anchor) -func set_shop_npc_player_in_range(in_range: bool) -> void: - if _shop_npc_player_in_range == in_range: +func set_shop_npc_player_in_range( + in_range: bool, + dialogue_text: String = "", +) -> void: + var dialogue_changed: bool = ( + not dialogue_text.is_empty() + and _shop_prompt_message.text != dialogue_text + ) + if dialogue_changed: + _shop_prompt_message.text = dialogue_text + _shop_prompt_message.visible_characters = -1 + _shop_npc_spoken_for_current_visit = false + if _shop_npc_player_in_range == in_range and not dialogue_changed: return _shop_npc_player_in_range = in_range if not in_range: @@ -1941,10 +2096,10 @@ func _try_start_shop_npc_speech() -> void: or not _gameplay_ui_enabled ): return - _shop_npc_spoken_for_current_visit = true var now_msec: int = Time.get_ticks_msec() if now_msec < _shop_npc_next_speech_msec: return + _shop_npc_spoken_for_current_visit = true _shop_npc_next_speech_msec = ( now_msec + SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS ) @@ -2003,7 +2158,7 @@ func _position_shop_prompt(world_anchor: Vector3) -> void: if _player == null: _shop_prompt.hide() return - var camera: Camera3D = _player.get_gameplay_camera() + var camera: Camera3D = _player.get_active_gameplay_camera() if camera == null or camera.is_position_behind(world_anchor): _shop_prompt.hide() return @@ -2207,6 +2362,12 @@ func _on_catch_display_changed( and _fishing_spot != null and _fishing_spot.is_fighting() ) + _update_fishing_feedback( + progress, + chase_progress, + barrier_health, + encounter_visible, + ) _green_catch_progress.value = progress * 100.0 _red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0 _catch_track.visible = encounter_visible @@ -2243,6 +2404,93 @@ func _on_catch_display_changed( _refresh_fishing_panel_visibility() +func _update_fishing_feedback( + progress: float, + chase_progress: float, + barrier_health: PackedInt32Array, + encounter_visible: bool, +) -> void: + if not encounter_visible: + _previous_barrier_health.clear() + _barrier_feedback_initialized = false + _fishing_danger_target = 0.0 + return + if _barrier_feedback_initialized: + var comparison_count: int = mini( + _previous_barrier_health.size(), + barrier_health.size(), + ) + var barrier_hit: bool = false + var barrier_broken: bool = false + for barrier_index: int in range(comparison_count): + if ( + barrier_health[barrier_index] + >= _previous_barrier_health[barrier_index] + ): + continue + barrier_hit = true + barrier_broken = ( + barrier_broken + or barrier_health[barrier_index] <= 0 + ) + if barrier_hit: + _play_barrier_feedback(barrier_broken) + _previous_barrier_health = barrier_health.duplicate() + _barrier_feedback_initialized = true + var chase_gap: float = progress - maxf(chase_progress, 0.0) + _fishing_danger_target = clampf( + inverse_lerp( + FISHING_DANGER_START_GAP, + FISHING_DANGER_FULL_GAP, + chase_gap, + ), + 0.0, + 1.0, + ) + + +func _play_barrier_feedback(barrier_broken: bool) -> void: + if _player != null: + _player.play_fishing_barrier_camera_feedback(barrier_broken) + if _catch_track_shake_tween != null: + _catch_track_shake_tween.kill() + _catch_track.rotation = 0.0 + var tilt: float = 0.018 if barrier_broken else 0.008 + var beat: float = 0.038 if barrier_broken else 0.028 + _catch_track_shake_tween = create_tween() + _catch_track_shake_tween.tween_property( + _catch_track, "rotation", -tilt, beat + ) + _catch_track_shake_tween.tween_property( + _catch_track, "rotation", tilt, beat + ) + _catch_track_shake_tween.tween_property( + _catch_track, "rotation", -tilt * 0.45, beat + ) + _catch_track_shake_tween.tween_property( + _catch_track, "rotation", 0.0, beat + ) + + +func _update_fishing_danger_vignette(delta: float) -> void: + _fishing_danger_pulse_time += delta + _fishing_danger_intensity = lerpf( + _fishing_danger_intensity, + _fishing_danger_target, + 1.0 - exp(-8.0 * delta), + ) + var pulse: float = ( + 0.88 + sin(_fishing_danger_pulse_time * 5.5) * 0.12 + ) + var shown_intensity: float = _fishing_danger_intensity * pulse + _fishing_danger_vignette.visible = shown_intensity > 0.01 + var vignette_material := ( + _fishing_danger_vignette.material as ShaderMaterial + ) + if vignette_material != null: + vignette_material.set_shader_parameter("intensity", shown_intensity) + + func _update_barrier_markers( positions: PackedFloat32Array, health: PackedInt32Array, @@ -2755,6 +3003,7 @@ func _refresh_hotbar_visibility() -> void: func _emit_interactive_pointer_ui_changed() -> void: _refresh_active_bait_indicator_visibility() + _refresh_rv_storage_button_visibility() interactive_pointer_ui_changed.emit( _system_menu_open or _player_menu_open diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 483433a..c5dea31 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=22 format=3] +[gd_scene load_steps=27 format=3] [ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"] [ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"] @@ -15,6 +15,13 @@ [ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"] [ext_resource type="PackedScene" path="res://ui/player_storage.tscn" id="14_storage"] [ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="15_social_prompt"] +[ext_resource type="PackedScene" path="res://ui/decor_shop.tscn" id="16_decor_shop"] +[ext_resource type="PackedScene" path="res://ui/home_decor_toolbar.tscn" id="17_decor_toolbar"] +[ext_resource type="PackedScene" path="res://ui/rv_upgrade_shop.tscn" id="18_rv_upgrade_shop"] +[ext_resource type="Shader" path="res://ui/fishing_danger_vignette.gdshader" id="19_fishing_vignette"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_fishing_vignette"] +shader = ExtResource("19_fishing_vignette") [sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"] bg_color = Color(0.032, 0.118, 0.15, 1) @@ -84,6 +91,20 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 +[node name="FishingDangerVignette" type="ColorRect" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] +unique_name_in_owner = true +visible = false +z_index = 50 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +material = SubResource("ShaderMaterial_fishing_vignette") +color = Color(1, 1, 1, 1) + [node name="ActiveBaitIndicator" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true z_index = 54 @@ -310,6 +331,7 @@ offset_right = 420.0 offset_bottom = -124.0 grow_horizontal = 2 grow_vertical = 0 +pivot_offset = Vector2(420, 18) mouse_filter = 2 [node name="GreenCatchProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/CatchTrack"] @@ -438,7 +460,7 @@ theme_override_constants/margin_bottom = 8 unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 18 -text = "Hey, if you've got any fish I've got cash! OwO" +text = "Listen, you may suck at fishing but I've got some things that might help." horizontal_alignment = 1 vertical_alignment = 1 autowrap_mode = 2 @@ -472,9 +494,40 @@ polygon = PackedVector2Array(-8, 0, 8, 0, 0, 10) [node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")] unique_name_in_owner = true +[node name="DecorShop" parent="UIRoot/CanonicalStage" instance=ExtResource("16_decor_shop")] +unique_name_in_owner = true + +[node name="RVUpgradeShop" parent="UIRoot/CanonicalStage" instance=ExtResource("18_rv_upgrade_shop")] +unique_name_in_owner = true + +[node name="HomeDecorToolbar" parent="UIRoot/CanonicalStage" instance=ExtResource("17_decor_toolbar")] +unique_name_in_owner = true + [node name="PlayerStorage" parent="UIRoot/CanonicalStage" instance=ExtResource("14_storage")] unique_name_in_owner = true +[node name="RVStorageButton" type="Button" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] +unique_name_in_owner = true +visible = false +z_index = 57 +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -74.0 +offset_top = 16.0 +offset_right = -16.0 +offset_bottom = 74.0 +grow_horizontal = 0 +mouse_default_cursor_shape = 2 +focus_mode = 0 +tooltip_text = "open RV storage" +theme = ExtResource("3_theme") +texture_filter = 1 +expand_icon = true +icon_alignment = 1 +vertical_icon_alignment = 1 + [node name="StoragePrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false diff --git a/ui/home_decor_toolbar.gd b/ui/home_decor_toolbar.gd new file mode 100644 index 0000000..50ae444 --- /dev/null +++ b/ui/home_decor_toolbar.gd @@ -0,0 +1,158 @@ +class_name HomeDecorToolbar +extends PanelContainer + +signal placement_mode_requested(mode: int) +signal rotate_requested +signal store_requested +signal cancel_requested + +var _mode_button: Button +var _selection_label: Label +var _rotate_button: Button +var _store_button: Button +var _clear_button: Button +var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID +var _interactive: bool = true +var _selection_active: bool = false + + +func _ready() -> void: + _build_ui() + hide() + + +func set_active(active: bool) -> void: + if visible == active: + return + visible = active + + +func set_interactive(interactive: bool) -> void: + _interactive = interactive + _refresh_button_interactivity() + + +func set_mode(mode: PlayerHomeState.PlacementMode) -> void: + _mode = mode + if _mode_button != null: + _mode_button.text = ( + "grid" + if mode == PlayerHomeState.PlacementMode.GRID + else "free" + ) + _mode_button.tooltip_text = "placement mode: %s" % _mode_button.text + + +func set_selection(instance_id: String, display_name: String = "") -> void: + var selected: bool = not instance_id.is_empty() + _selection_active = selected + if _selection_label != null: + _selection_label.text = ( + "selected: %s" % display_name + if selected else "use held decor to place; click or drag to edit" + ) + _refresh_button_interactivity() + + +func report_status(message: String) -> void: + if _selection_label != null: + _selection_label.text = message + + +func _build_ui() -> void: + set_anchors_preset(Control.PRESET_TOP_RIGHT) + offset_left = -500.0 + offset_top = 0.0 + offset_right = 0.0 + offset_bottom = 106.0 + custom_minimum_size = Vector2(500.0, 0.0) + z_index = 85 + mouse_filter = Control.MOUSE_FILTER_STOP + var panel_style: StyleBoxFlat = UtilityPageStyle.panel_style() + panel_style.set_corner_radius_all(10) + panel_style.content_margin_left = 6.0 + panel_style.content_margin_top = 6.0 + panel_style.content_margin_right = 6.0 + panel_style.content_margin_bottom = 6.0 + add_theme_stylebox_override("panel", panel_style) + var margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + margin.add_theme_constant_override(side, 6) + add_child(margin) + var layout := VBoxContainer.new() + layout.add_theme_constant_override("separation", 5) + margin.add_child(layout) + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 5) + layout.add_child(row) + var title := Label.new() + title.text = "decor" + title.custom_minimum_size = Vector2(70.0, 46.0) + title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title.add_theme_font_size_override("font_size", 19) + row.add_child(title) + _mode_button = _add_tool_button(row, "grid", "switch placement mode", 82.0) + _mode_button.pressed.connect(_toggle_mode) + _rotate_button = _add_tool_button(row, "rotate", "rotate selected decor", 92.0) + _rotate_button.pressed.connect(rotate_requested.emit) + _store_button = _add_tool_button(row, "pack", "pack selected decor", 82.0) + _store_button.pressed.connect(store_requested.emit) + _clear_button = _add_tool_button(row, "clear", "clear selection", 82.0) + _clear_button.pressed.connect(cancel_requested.emit) + _selection_label = Label.new() + _selection_label.text = "use held decor to place; click or drag to edit" + _selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _selection_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _selection_label.add_theme_font_size_override("font_size", 15) + layout.add_child(_selection_label) + _refresh_button_interactivity() + + +func _add_tool_button( + parent: Container, + button_text: String, + button_tooltip: String, + minimum_width: float, +) -> Button: + var button := Button.new() + button.text = button_text + button.tooltip_text = button_tooltip + button.accessibility_name = button_tooltip + button.custom_minimum_size = Vector2(minimum_width, 46.0) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + UtilityPageStyle.apply_ocean_button(button) + for style_name: StringName in [ + &"normal", &"hover", &"pressed", &"focus", &"disabled", + ]: + var existing: StyleBox = button.get_theme_stylebox(style_name) + var rounded := existing.duplicate() as StyleBoxFlat + if rounded == null: + continue + rounded.set_corner_radius_all(20) + button.add_theme_stylebox_override(style_name, rounded) + parent.add_child(button) + return button + + +func _refresh_button_interactivity() -> void: + if _mode_button != null: + _mode_button.disabled = not _interactive + if _rotate_button != null: + _rotate_button.disabled = not _interactive or not _selection_active + if _store_button != null: + _store_button.disabled = not _interactive or not _selection_active + if _clear_button != null: + _clear_button.disabled = not _interactive or not _selection_active + + +func _toggle_mode() -> void: + _mode = ( + PlayerHomeState.PlacementMode.FREE + if _mode == PlayerHomeState.PlacementMode.GRID + else PlayerHomeState.PlacementMode.GRID + ) + set_mode(_mode) + placement_mode_requested.emit(int(_mode)) diff --git a/ui/home_decor_toolbar.gd.uid b/ui/home_decor_toolbar.gd.uid new file mode 100644 index 0000000..b1bb23b --- /dev/null +++ b/ui/home_decor_toolbar.gd.uid @@ -0,0 +1 @@ +uid://bmtlixw3wief3 diff --git a/ui/home_decor_toolbar.tscn b/ui/home_decor_toolbar.tscn new file mode 100644 index 0000000..671308d --- /dev/null +++ b/ui/home_decor_toolbar.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/home_decor_toolbar.gd" id="1_script"] + +[node name="HomeDecorToolbar" type="PanelContainer"] +unique_name_in_owner = true +visible = false +script = ExtResource("1_script") diff --git a/ui/hotbar.gd b/ui/hotbar.gd index 24a9aa8..5f54217 100644 --- a/ui/hotbar.gd +++ b/ui/hotbar.gd @@ -37,6 +37,7 @@ var _slots: Array[BubbleHotbarSlotType] = [] var _gameplay_input_enabled: bool = false var _drag_enabled: bool = false var _swap_hotbar_camera_scroll: bool = false +var _rv_interior_camera_mode: bool = false var _motion_elapsed: float = 0.0 var _compact_layout: bool = false var _player_menu_context: bool = false @@ -99,6 +100,10 @@ func set_swap_hotbar_camera_scroll(enabled: bool) -> void: _swap_hotbar_camera_scroll = enabled +func set_rv_interior_camera_mode(enabled: bool) -> void: + _rv_interior_camera_mode = enabled + + func set_drag_enabled(enabled: bool) -> void: _drag_enabled = enabled for slot: BubbleHotbarSlotType in _slots: @@ -308,7 +313,11 @@ func _unhandled_input(event: InputEvent) -> void: if ( event is InputEventMouseButton and event.pressed - and event.shift_pressed == _swap_hotbar_camera_scroll + and ( + event.shift_pressed + if _rv_interior_camera_mode + else event.shift_pressed == _swap_hotbar_camera_scroll + ) ): if event.button_index == MOUSE_BUTTON_WHEEL_UP: _hotbar.cycle_selection(-1) diff --git a/ui/icons/new_menu/close.png b/ui/icons/new_menu/close.png new file mode 100644 index 0000000..886ed6a Binary files /dev/null and b/ui/icons/new_menu/close.png differ diff --git a/ui/icons/new_menu/close.png.import b/ui/icons/new_menu/close.png.import new file mode 100644 index 0000000..8b696a8 --- /dev/null +++ b/ui/icons/new_menu/close.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bq8a7n8grdob3" +path.s3tc="res://.godot/imported/close.png-6d4985cc89fb9a07db5c6ce09f05566a.s3tc.ctex" +path.etc2="res://.godot/imported/close.png-6d4985cc89fb9a07db5c6ce09f05566a.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://ui/icons/new_menu/close.png" +dest_files=["res://.godot/imported/close.png-6d4985cc89fb9a07db5c6ce09f05566a.s3tc.ctex", "res://.godot/imported/close.png-6d4985cc89fb9a07db5c6ce09f05566a.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/ui/icons/new_menu/credits.png b/ui/icons/new_menu/credits.png new file mode 100644 index 0000000..76b75e3 Binary files /dev/null and b/ui/icons/new_menu/credits.png differ diff --git a/ui/icons/new_menu/credits.png.import b/ui/icons/new_menu/credits.png.import new file mode 100644 index 0000000..4351ef6 --- /dev/null +++ b/ui/icons/new_menu/credits.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://br1f2ofv4575c" +path.s3tc="res://.godot/imported/credits.png-636657da1c8dc4ba52732813af1a703c.s3tc.ctex" +path.etc2="res://.godot/imported/credits.png-636657da1c8dc4ba52732813af1a703c.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://ui/icons/new_menu/credits.png" +dest_files=["res://.godot/imported/credits.png-636657da1c8dc4ba52732813af1a703c.s3tc.ctex", "res://.godot/imported/credits.png-636657da1c8dc4ba52732813af1a703c.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/ui/icons/new_menu/online.png b/ui/icons/new_menu/online.png new file mode 100644 index 0000000..2274926 Binary files /dev/null and b/ui/icons/new_menu/online.png differ diff --git a/ui/icons/new_menu/online.png.import b/ui/icons/new_menu/online.png.import new file mode 100644 index 0000000..df12baa --- /dev/null +++ b/ui/icons/new_menu/online.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c8sty10uue6nm" +path.s3tc="res://.godot/imported/online.png-3f69c41cde4beb29d8f23e574a61641f.s3tc.ctex" +path.etc2="res://.godot/imported/online.png-3f69c41cde4beb29d8f23e574a61641f.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://ui/icons/new_menu/online.png" +dest_files=["res://.godot/imported/online.png-3f69c41cde4beb29d8f23e574a61641f.s3tc.ctex", "res://.godot/imported/online.png-3f69c41cde4beb29d8f23e574a61641f.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/ui/icons/new_menu/play.png b/ui/icons/new_menu/play.png new file mode 100644 index 0000000..aa61c52 Binary files /dev/null and b/ui/icons/new_menu/play.png differ diff --git a/ui/assets/title/netfishing_logo.png.import b/ui/icons/new_menu/play.png.import similarity index 55% rename from ui/assets/title/netfishing_logo.png.import rename to ui/icons/new_menu/play.png.import index 2aa351a..6fc788b 100644 --- a/ui/assets/title/netfishing_logo.png.import +++ b/ui/icons/new_menu/play.png.import @@ -2,20 +2,22 @@ importer="texture" type="CompressedTexture2D" -uid="uid://ckqayis1ckb2v" -path="res://.godot/imported/netfishing_logo.png-82ab9fce5851dcc5239eca178e8cde99.ctex" +uid="uid://7qe6siny4ru7" +path.s3tc="res://.godot/imported/play.png-f35bbfc9549b806b0a83d0c3b4c7dc74.s3tc.ctex" +path.etc2="res://.godot/imported/play.png-f35bbfc9549b806b0a83d0c3b4c7dc74.etc2.ctex" metadata={ -"vram_texture": false +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true } [deps] -source_file="res://ui/assets/title/netfishing_logo.png" -dest_files=["res://.godot/imported/netfishing_logo.png-82ab9fce5851dcc5239eca178e8cde99.ctex"] +source_file="res://ui/icons/new_menu/play.png" +dest_files=["res://.godot/imported/play.png-f35bbfc9549b806b0a83d0c3b4c7dc74.s3tc.ctex", "res://.godot/imported/play.png-f35bbfc9549b806b0a83d0c3b4c7dc74.etc2.ctex"] [params] -compress/mode=0 +compress/mode=2 compress/high_quality=false compress/lossy_quality=0.7 compress/uastc_level=0 @@ -37,4 +39,4 @@ 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 +detect_3d/compress_to=0 diff --git a/ui/icons/new_menu/settings.png b/ui/icons/new_menu/settings.png new file mode 100644 index 0000000..7d69c3b Binary files /dev/null and b/ui/icons/new_menu/settings.png differ diff --git a/ui/icons/new_menu/settings.png.import b/ui/icons/new_menu/settings.png.import new file mode 100644 index 0000000..54dde27 --- /dev/null +++ b/ui/icons/new_menu/settings.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cxqwvyg0qryg1" +path.s3tc="res://.godot/imported/settings.png-3654f1ee86a2237bbc3c9af7e818343c.s3tc.ctex" +path.etc2="res://.godot/imported/settings.png-3654f1ee86a2237bbc3c9af7e818343c.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://ui/icons/new_menu/settings.png" +dest_files=["res://.godot/imported/settings.png-3654f1ee86a2237bbc3c9af7e818343c.s3tc.ctex", "res://.godot/imported/settings.png-3654f1ee86a2237bbc3c9af7e818343c.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/ui/logbook_catalog.gd b/ui/logbook_catalog.gd index d5dc639..ac7a910 100644 --- a/ui/logbook_catalog.gd +++ b/ui/logbook_catalog.gd @@ -2,6 +2,9 @@ class_name LogbookCatalog extends RefCounted const FishDataType = preload("res://fish/fish_data.gd") +const FishInventoryType = preload("res://inventory/fish_inventory.gd") +const CollectionLogType = preload("res://collection/collection_log.gd") +const CalendarSeasonType = preload("res://world/calendar_season.gd") enum Category { FRESH_WATER, @@ -10,6 +13,66 @@ enum Category { SHELLFISH, } +enum DiscoveryFilter { + ALL, + CAUGHT, + MISSING, + INCOMPLETE, +} + +enum SortMode { + CATALOG_NUMBER, + NAME, + RARITY, + CATCH_COUNT, + QUALITY_PROGRESS, + VALUE, + WEIGHT, +} + +enum AvailabilityFilter { + ANY, + AVAILABLE_NOW, +} + +enum SeasonFilter { + ANY, + CURRENT, + SPRING, + SUMMER, + FALL, + WINTER, +} + +enum TimeFilter { + ANY, + CURRENT, + DAY, + NIGHT, +} + +enum RarityFilter { + ANY, + COMMON, + UNCOMMON, + RARE, + EPIC, + LEGENDARY, +} + +enum MethodFilter { + ANY, + FISHING, + NET, + DIGGING, +} + +enum HoldingFilter { + ANY, + IN_COOLER, + FAVORITED, +} + static func category_for(fish: FishDataType) -> Category: if fish == null: @@ -73,3 +136,278 @@ static func ordered_species( return a.catalog_number < b.catalog_number ) return ordered + + +static func filtered_species( + candidates: Array[FishDataType], + category: Category, + collection_log: CollectionLogType, + inventory: FishInventoryType, + options: Dictionary, + available_now_resolver: Callable = Callable(), + current_season: int = CalendarSeasonType.UNKNOWN, + current_is_night: bool = false, +) -> Array[FishDataType]: + var filtered: Array[FishDataType] = [] + for fish: FishDataType in ordered_species(candidates): + if category_for(fish) != category: + continue + if not _matches_filters( + fish, + collection_log, + inventory, + options, + available_now_resolver, + current_season, + current_is_night, + ): + continue + filtered.append(fish) + var sort_mode := int(options.get("sort", SortMode.CATALOG_NUMBER)) + var descending := bool(options.get("descending", false)) + filtered.sort_custom( + func(first: FishDataType, second: FishDataType) -> bool: + return _species_less_than( + first, + second, + collection_log, + sort_mode, + descending, + ) + ) + return filtered + + +static func available_groups( + candidates: Array[FishDataType], + category: Category, + _collection_log: CollectionLogType, +) -> Array[String]: + var groups: Array[String] = [] + for fish: FishDataType in ordered_species(candidates): + if category_for(fish) != category: + continue + var group := _group_label(fish) + if not group.is_empty() and group not in groups: + groups.append(group) + groups.sort_custom( + func(first: String, second: String) -> bool: + return first.naturalnocasecmp_to(second) < 0 + ) + return groups + + +static func quality_progress( + collection_log: CollectionLogType, + fish_id: StringName, +) -> int: + if collection_log == null: + return 0 + var mask: int = collection_log.get_quality_mask(fish_id) + var count: int = 0 + while mask > 0: + count += mask & 1 + mask >>= 1 + return count + + +static func _matches_filters( + fish: FishDataType, + collection_log: CollectionLogType, + inventory: FishInventoryType, + options: Dictionary, + available_now_resolver: Callable, + current_season: int, + current_is_night: bool, +) -> bool: + var discovered := ( + collection_log != null and collection_log.has_discovered(fish.id) + ) + match int(options.get("show", DiscoveryFilter.ALL)): + DiscoveryFilter.CAUGHT: + if not discovered: + return false + DiscoveryFilter.MISSING: + if discovered: + return false + DiscoveryFilter.INCOMPLETE: + if ( + not discovered + or collection_log.has_mastered(fish.id) + ): + return false + + var search_text := str(options.get("search", "")).strip_edges().to_lower() + if not search_text.is_empty(): + var number_text := str(catalog_number(fish)) + var number_query := search_text.trim_prefix("#") + var number_matches := ( + number_query.is_valid_int() + and number_text == number_query + ) + var identity_matches := false + if discovered: + identity_matches = " ".join([ + fish.display_name, + _group_label(fish), + fish.get_habitat_label(), + ]).to_lower().contains(search_text) + if not number_matches and not identity_matches: + return false + + if ( + int(options.get("availability", AvailabilityFilter.ANY)) + == AvailabilityFilter.AVAILABLE_NOW + ): + if available_now_resolver.is_valid(): + var resolved: Variant = available_now_resolver.call(fish) + if typeof(resolved) != TYPE_BOOL or not bool(resolved): + return false + elif not _matches_season(fish, current_season): + return false + + var season_filter := int(options.get("season", SeasonFilter.ANY)) + if season_filter != SeasonFilter.ANY: + var requested_season := current_season + if season_filter >= SeasonFilter.SPRING: + requested_season = season_filter - SeasonFilter.SPRING + if not _matches_season(fish, requested_season): + return false + + var time_filter := int(options.get("time", TimeFilter.ANY)) + if time_filter != TimeFilter.ANY: + var requested_night := current_is_night + if time_filter == TimeFilter.DAY: + requested_night = false + elif time_filter == TimeFilter.NIGHT: + requested_night = true + if not _matches_time(fish, requested_night): + return false + + var rarity_filter := int(options.get("rarity", RarityFilter.ANY)) + if ( + rarity_filter != RarityFilter.ANY + and fish.rarity != rarity_filter - RarityFilter.COMMON + ): + return false + + var method_filter := int(options.get("method", MethodFilter.ANY)) + if ( + method_filter != MethodFilter.ANY + and fish.collection_method != method_filter - MethodFilter.FISHING + ): + return false + + var holding_filter := int(options.get("holding", HoldingFilter.ANY)) + if holding_filter != HoldingFilter.ANY: + var favorites_only := holding_filter == HoldingFilter.FAVORITED + if not _has_current_catch(inventory, fish.id, favorites_only): + return false + + var requested_group := str(options.get("group", "")) + if ( + not requested_group.is_empty() + and _group_label(fish).nocasecmp_to(requested_group) != 0 + ): + return false + return true + + +static func _species_less_than( + first: FishDataType, + second: FishDataType, + collection_log: CollectionLogType, + sort_mode: int, + descending: bool, +) -> bool: + var first_discovered := ( + collection_log != null and collection_log.has_discovered(first.id) + ) + var second_discovered := ( + collection_log != null and collection_log.has_discovered(second.id) + ) + if sort_mode != SortMode.CATALOG_NUMBER: + if first_discovered != second_discovered: + return first_discovered + if not first_discovered and not second_discovered: + return _catalog_less_than(first, second) + + var comparison: int = 0 + match sort_mode: + SortMode.NAME: + comparison = first.display_name.naturalnocasecmp_to( + second.display_name + ) + SortMode.RARITY: + comparison = first.rarity - second.rarity + SortMode.CATCH_COUNT: + comparison = ( + collection_log.get_catch_count(first.id) + - collection_log.get_catch_count(second.id) + ) + SortMode.QUALITY_PROGRESS: + comparison = ( + quality_progress(collection_log, first.id) + - quality_progress(collection_log, second.id) + ) + SortMode.VALUE: + comparison = first.sell_value_max - second.sell_value_max + SortMode.WEIGHT: + comparison = _compare_floats( + first.get_maximum_weight(), + second.get_maximum_weight(), + ) + _: + comparison = first.catalog_number - second.catalog_number + if comparison != 0: + return comparison > 0 if descending else comparison < 0 + return _catalog_less_than(first, second) + + +static func _catalog_less_than(first: FishDataType, second: FishDataType) -> bool: + if first.catalog_number == second.catalog_number: + return String(first.id) < String(second.id) + return first.catalog_number < second.catalog_number + + +static func _compare_floats(first: float, second: float) -> int: + if is_equal_approx(first, second): + return 0 + return -1 if first < second else 1 + + +static func _matches_season(fish: FishDataType, season: int) -> bool: + return fish.is_available_in_season(season) + + +static func _matches_time(fish: FishDataType, requested_night: bool) -> bool: + if ( + fish.collection_method != FishDataType.CollectionMethod.FISHING + or fish.availability == null + ): + return true + return ( + fish.availability.allow_night + if requested_night + else fish.availability.allow_day + ) + + +static func _has_current_catch( + inventory: FishInventoryType, + fish_id: StringName, + favorites_only: bool, +) -> bool: + if inventory == null: + return false + for fish_catch in inventory.get_catches_by_fish_id(fish_id): + if fish_catch != null and (not favorites_only or fish_catch.is_favorited): + return true + return false + + +static func _group_label(fish: FishDataType) -> String: + if fish == null: + return "" + var group := String(fish.collection_group).strip_edges() + return group if not group.is_empty() else fish.get_habitat_label() diff --git a/ui/logbook_page.gd b/ui/logbook_page.gd index c5528e4..cc490a0 100644 --- a/ui/logbook_page.gd +++ b/ui/logbook_page.gd @@ -6,10 +6,21 @@ const FishDataType = preload("res://fish/fish_data.gd") const FishQualityType = preload("res://fish/fish_quality.gd") const FishInventoryType = preload("res://inventory/fish_inventory.gd") const FishPoolType = preload("res://fish/fish_pool.gd") +const FishingSpotType = preload("res://fishing/fishing_spot.gd") +const WorldTimeServiceType = preload("res://world/world_time_service.gd") +const WorldWeatherServiceType = preload( + "res://world/world_weather_service.gd" +) const SILHOUETTE_SHADER: Shader = preload( "res://ui/logbook_silhouette.gdshader" ) const OrganizerTabType = preload("res://ui/components/organizer_tab.gd") +const ControllerFocusNavigationType = preload( + "res://ui/controller_focus_navigation.gd" +) +const NOTEPAD_INK_ACTION_SCENE: PackedScene = preload( + "res://ui/components/bubble_menu/notepad_ink_action.tscn" +) const InventoryNotepadType = preload( "res://ui/components/inventory_notepad.gd" ) @@ -48,6 +59,8 @@ const LOGBOOK_TAB_LEFT_INSET: float = 92.0 const LOGBOOK_TAB_SIZE := Vector2(108.0, 38.0) const LOGBOOK_TAB_SEPARATION: float = 6.0 const LOGBOOK_TAB_TEXT_SIDE_INSET: float = 10.0 +const INDEX_TAB_SIZE := Vector2(88.0, 38.0) +const INDEX_TAB_RIGHT_INSET: float = 72.0 const PAGE_CONTENT_SCALE: float = 0.97 const WIDE_OUTER_MARGINS := Rect2(52.0, 85.0, 52.0, 18.0) const COMPACT_OUTER_MARGINS := Rect2(160.0, 85.0, 160.0, 106.0) @@ -56,23 +69,58 @@ const COMPACT_BOOK_TOP: float = 65.0 const CATALOG_PORTRAIT_SIZE := Vector2(78.0, 36.0) const CATALOG_ENTRY_SIZE := Vector2(92.0, 92.0) const CATALOG_ROW_STEP: float = 98.0 -const CATALOG_SNAP_DELAY: float = 0.12 const DETAIL_PORTRAIT_SIZE := Vector2(160.0, 88.0) const DETAIL_SECTION_SEPARATION: int = 10 const DETAIL_QUALITY_SECTION_SIZE := Vector2(300.0, 104.0) const DETAIL_STATS_SECTION_SIZE := Vector2(400.0, 160.0) const DETAIL_STATS_ROW_SEPARATION: int = 8 +const INDEX_OPTION_HEIGHT: float = 34.0 +const INDEX_CONTENT_LEFT_INSET: float = 44.0 +const INDEX_CONTENT_TOP_INSET: float = 4.0 +const INDEX_CONTENT_RIGHT_INSET: float = 22.0 +const INDEX_CONTENT_BOTTOM_INSET: float = 10.0 +const INDEX_SHOW_LABELS := [ + "all entries", "caught", "missing", "needs quality", +] +const INDEX_SORT_LABELS := [ + "catalog no.", "name", "rarity", "number caught", + "quality progress", "value", "weight", +] +const INDEX_AVAILABILITY_LABELS := [ + "any time", "available now", +] +const INDEX_SEASON_LABELS := [ + "any season", "current season", "spring", "summer", "fall", "winter", +] +const INDEX_TIME_LABELS := [ + "any time", "current time", "day", "night", +] +const INDEX_RARITY_LABELS := [ + "any rarity", "common", "uncommon", "rare", "epic", "legendary", +] +const INDEX_METHOD_LABELS := [ + "any method", "fishing", "netting", "digging", +] +const INDEX_HOLDING_LABELS := [ + "any ownership", "in cooler", "favorited", +] + +signal controller_text_entry_requested(control: Control) enum ControllerZone { TABS, ENTRIES, DETAILS, + INDEX, OVERLAY, } var _collection_log: CollectionLogType var _inventory: FishInventoryType var _catalog: FishPoolType +var _fishing_spot: FishingSpotType +var _world_time: WorldTimeServiceType +var _world_weather: WorldWeatherServiceType var _category: LogbookCatalog.Category = ( LogbookCatalog.Category.FRESH_WATER ) @@ -86,8 +134,6 @@ var _detail_tween: Tween var _category_generation: int = 0 var _category_tween: Tween var _silhouette_material: ShaderMaterial -var _snapping_catalog_scroll: bool = false -var _catalog_scroll_snap_timer: Timer var _outer_margin: MarginContainer var _book: Control var _compact_presentation: bool = false @@ -114,6 +160,31 @@ var _portrait_overlay_stats_view: Control var _portrait_overlay_stats: GridContainer var _overlay_return_focus: Control var _controller_zone: ControllerZone = ControllerZone.TABS +var _index_tab: OrganizerTabType +var _index_body: VBoxContainer +var _index_search: LineEdit +var _index_summary: Label +var _index_note: Label +var _index_reset: Button +var _index_option_buttons: Dictionary[StringName, Button] = {} +var _index_controls: Array[Control] = [] +var _index_open: bool = false +var _index_update_in_progress: bool = false +var _index_visible_count: int = 0 +var _index_category_total: int = 0 +var _index_options: Dictionary = { + "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 _ready() -> void: @@ -127,10 +198,16 @@ func setup( collection_log: CollectionLogType, inventory: FishInventoryType, catalog: FishPoolType, + fishing_spot: FishingSpotType = null, + world_time: WorldTimeServiceType = null, + world_weather: WorldWeatherServiceType = null, ) -> void: _collection_log = collection_log _inventory = inventory _catalog = catalog + _fishing_spot = fishing_spot + _world_time = world_time + _world_weather = world_weather if not _collection_log.fish_discovered.is_connected( _on_fish_discovered ): @@ -141,12 +218,28 @@ func setup( _collection_log.collection_changed.connect(_on_collection_changed) if not _inventory.catches_changed.is_connected(_on_inventory_changed): _inventory.catches_changed.connect(_on_inventory_changed) + if ( + _world_time != null + and not _world_time.time_changed.is_connected( + _on_logbook_context_changed + ) + ): + _world_time.time_changed.connect(_on_logbook_context_changed) + _world_time.calendar_date_changed.connect(_on_logbook_context_changed) + if ( + _world_weather != null + and not _world_weather.weather_changed.is_connected( + _on_logbook_context_changed + ) + ): + _world_weather.weather_changed.connect(_on_logbook_context_changed) _refresh_catalog() func set_compact_presentation(compact: bool) -> void: _compact_presentation = compact _apply_presentation_margins() + _apply_index_presentation() func activate() -> void: @@ -183,6 +276,17 @@ func set_interactive(value: bool) -> void: if _interactive and not tab.button_pressed else Control.MOUSE_FILTER_IGNORE ) + if _index_tab != null: + _index_tab.focus_mode = ( + Control.FOCUS_ALL + if _interactive and _controller_zone == ControllerZone.TABS + else Control.FOCUS_NONE + ) + _index_tab.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if _interactive + else Control.MOUSE_FILTER_IGNORE + ) for entry: Button in _entry_buttons.values(): entry.focus_mode = ( Control.FOCUS_ALL @@ -202,6 +306,19 @@ func set_interactive(value: bool) -> void: if _interactive and _controller_zone == ControllerZone.DETAILS else Control.FOCUS_NONE ) + for index_control: Control in _index_controls: + if not is_instance_valid(index_control): + continue + index_control.focus_mode = ( + Control.FOCUS_ALL + if _interactive and _controller_zone == ControllerZone.INDEX + else Control.FOCUS_NONE + ) + index_control.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if _interactive and _index_open + else Control.MOUSE_FILTER_IGNORE + ) if _portrait_overlay_backdrop != null: _portrait_overlay_backdrop.focus_mode = ( Control.FOCUS_ALL @@ -217,6 +334,9 @@ func focus_initial() -> void: _portrait_overlay_backdrop.grab_focus() return if _controller_zone == ControllerZone.TABS: + if _index_open and _index_tab != null: + _index_tab.grab_focus() + return var category_tab_index: int = _category_tab_categories.find(_category) if category_tab_index >= 0: _category_tabs[category_tab_index].grab_focus() @@ -225,6 +345,10 @@ func focus_initial() -> void: if not _detail_buttons.is_empty(): _detail_buttons.front().grab_focus() return + if _controller_zone == ControllerZone.INDEX: + if _index_search != null: + _index_search.grab_focus() + return var selected := _entry_buttons.get(_selected_entry_key) as Button if selected != null: selected.grab_focus() @@ -250,6 +374,8 @@ func handle_controller_input(event: InputEvent) -> bool: _hide_portrait_overlay() ControllerZone.DETAILS: _set_controller_zone(ControllerZone.ENTRIES) + ControllerZone.INDEX: + _set_index_open(false) ControllerZone.ENTRIES: _set_controller_zone(ControllerZone.TABS) ControllerZone.TABS: @@ -257,15 +383,43 @@ func handle_controller_input(event: InputEvent) -> bool: return true if _controller_zone == ControllerZone.TABS: if event.is_action_pressed("ui_left"): + if _index_open: + _set_index_open(false, false) + _category_tabs.back().grab_focus() + return true _select_adjacent_controller_category(-1) return true if event.is_action_pressed("ui_right"): + if ( + _category_tab_categories.find(_category) + == _category_tab_categories.size() - 1 + ): + _set_index_open(true, false) + _index_tab.grab_focus() + return true _select_adjacent_controller_category(1) return true if accept_pressed: - if not _entry_buttons.is_empty(): + if _index_open: + _set_controller_zone(ControllerZone.INDEX) + elif not _entry_buttons.is_empty(): _set_controller_zone(ControllerZone.ENTRIES) return true + if _controller_zone == ControllerZone.INDEX: + var focus_owner := get_viewport().gui_get_focus_owner() + if accept_pressed and focus_owner == _index_search: + controller_text_entry_requested.emit(_index_search) + return true + if event.is_action_pressed("ui_left"): + var previous_key := _index_key_for_control(focus_owner) + if not previous_key.is_empty(): + _cycle_index_option(previous_key, -1) + return true + if event.is_action_pressed("ui_right") or accept_pressed: + var next_key := _index_key_for_control(focus_owner) + if not next_key.is_empty(): + _cycle_index_option(next_key, 1) + return true if _controller_zone == ControllerZone.ENTRIES and accept_pressed: if not _selected_entry_key.is_empty() and not _detail_buttons.is_empty(): _set_controller_zone(ControllerZone.DETAILS) @@ -332,6 +486,20 @@ func _build_interface() -> void: tabs.add_child(tab) _category_tabs.append(tab) _category_tab_categories.append(category) + _index_tab = OrganizerTabType.new() + _index_tab.name = "CatalogIndexTab" + _index_tab.anchor_left = 1.0 + _index_tab.anchor_right = 1.0 + _index_tab.offset_left = -INDEX_TAB_RIGHT_INSET - INDEX_TAB_SIZE.x + _index_tab.offset_top = 37.0 + _index_tab.offset_right = -INDEX_TAB_RIGHT_INSET + _index_tab.offset_bottom = 37.0 + INDEX_TAB_SIZE.y + _index_tab.text = "index" + _index_tab.palette_index = 0 + _index_tab.tooltip_text = "Open catalog index" + _index_tab.accessibility_name = "Catalog index" + _index_tab.pressed.connect(_toggle_index_page) + stack.add_child(_index_tab) _configure_category_focus() var book := Control.new() @@ -421,9 +589,176 @@ func _build_interface() -> void: right_page.add_child(_detail_body) _apply_page_content_scale(_detail_body) _show_no_selection() + _build_index_page(right_page) _build_portrait_overlay() +func _build_index_page(right_page: Control) -> void: + _index_body = VBoxContainer.new() + _index_body.name = "CatalogIndex" + _index_body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _index_body.offset_left = INDEX_CONTENT_LEFT_INSET + _index_body.offset_top = INDEX_CONTENT_TOP_INSET + _index_body.offset_right = -INDEX_CONTENT_RIGHT_INSET + _index_body.offset_bottom = -INDEX_CONTENT_BOTTOM_INSET + _index_body.add_theme_constant_override("separation", 4) + _index_body.visible = false + right_page.add_child(_index_body) + _apply_page_content_scale(_index_body) + _apply_index_presentation() + + var heading := _label("catalog index", 28) + heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _index_body.add_child(heading) + _index_summary = _field_label("", 14) + _index_summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _index_summary.add_theme_color_override("font_color", MUTED_INK) + _index_body.add_child(_index_summary) + + var search_stack := VBoxContainer.new() + search_stack.add_theme_constant_override("separation", 0) + _index_body.add_child(search_stack) + var search_label := _field_label("find by name, group, or catalog no.", 13) + search_label.add_theme_color_override("font_color", MUTED_INK) + search_stack.add_child(search_label) + _index_search = LineEdit.new() + _index_search.name = "CatalogIndexSearch" + _index_search.custom_minimum_size.y = 34.0 + _index_search.placeholder_text = "write here..." + _index_search.max_length = 48 + _index_search.add_theme_font_override( + "font", InventoryNotepadType.HANDWRITTEN_FONT + ) + _index_search.add_theme_font_size_override("font_size", 18) + _index_search.add_theme_color_override("font_color", INK) + _index_search.add_theme_color_override("caret_color", INK) + _index_search.add_theme_color_override( + "font_placeholder_color", Color(MUTED_INK, 0.72) + ) + for style_name: StringName in [ + &"normal", &"focus", &"read_only", + ]: + _index_search.add_theme_stylebox_override( + style_name, + _index_search_style(style_name == &"focus"), + ) + _index_search.text_changed.connect(_on_index_search_changed) + search_stack.add_child(_index_search) + _index_controls.append(_index_search) + + var options_grid := GridContainer.new() + options_grid.columns = 2 + options_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL + options_grid.add_theme_constant_override("h_separation", 18) + options_grid.add_theme_constant_override("v_separation", 2) + _index_body.add_child(options_grid) + for descriptor: Dictionary in [ + {"key": &"show", "label": "show"}, + {"key": &"rarity", "label": "rarity"}, + {"key": &"sort", "label": "sort by"}, + {"key": &"availability", "label": "availability"}, + {"key": &"descending", "label": "order"}, + {"key": &"season", "label": "season"}, + {"key": &"method", "label": "method"}, + {"key": &"time", "label": "time of day"}, + {"key": &"holding", "label": "ownership"}, + {"key": &"group", "label": "collection group"}, + ]: + options_grid.add_child(_make_index_option( + descriptor["key"], + str(descriptor["label"]), + )) + + var footer := HBoxContainer.new() + footer.alignment = BoxContainer.ALIGNMENT_CENTER + _index_body.add_child(footer) + _index_reset = NOTEPAD_INK_ACTION_SCENE.instantiate() as Button + _index_reset.name = "CatalogIndexReset" + _index_reset.custom_minimum_size = Vector2(126.0, 34.0) + _index_reset.text = "clear index" + _apply_index_value_font(_index_reset) + _index_reset.tooltip_text = "Clear every catalog filter" + _index_reset.accessibility_name = "Clear catalog index filters" + _index_reset.pressed.connect(_reset_index) + footer.add_child(_index_reset) + _index_controls.append(_index_reset) + + _index_note = _field_label( + "silhouettes keep names hidden in filtered results", + 12, + ) + _index_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _index_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _index_note.add_theme_color_override("font_color", Color(MUTED_INK, 0.82)) + _index_body.add_child(_index_note) + _update_index_controls() + _configure_index_focus.call_deferred() + + +func _make_index_option(key: StringName, label_text: String) -> Control: + var stack := VBoxContainer.new() + stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL + stack.add_theme_constant_override("separation", -3) + var label := _field_label(label_text, 12) + label.add_theme_color_override("font_color", MUTED_INK) + stack.add_child(label) + var button := NOTEPAD_INK_ACTION_SCENE.instantiate() as Button + button.name = "CatalogIndex%s" % String(key).capitalize().replace(" ", "") + button.custom_minimum_size = Vector2(0.0, INDEX_OPTION_HEIGHT) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.clip_text = true + _apply_index_value_font(button) + button.tooltip_text = "Change %s; use left or right on a controller" % label_text + button.pressed.connect(_cycle_index_option.bind(key, 1)) + button.gui_input.connect(_on_index_option_gui_input.bind(key)) + stack.add_child(button) + _index_option_buttons[key] = button + _index_controls.append(button) + return stack + + +func _apply_index_value_font(button: Button) -> void: + button.add_theme_font_override( + "font", InventoryNotepadType.HANDWRITTEN_FONT + ) + button.add_theme_font_size_override("font_size", 18) + for color_name: StringName in [ + &"font_color", + &"font_hover_color", + &"font_focus_color", + &"font_pressed_color", + ]: + button.add_theme_color_override(color_name, INK) + + +func _index_search_style(focused: bool) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = Color(1.0, 0.97, 0.82, 0.12 if focused else 0.0) + style.border_color = Color(INK, 0.72 if focused else 0.42) + style.border_width_bottom = 2 + style.content_margin_left = 6 + style.content_margin_right = 22 + style.content_margin_top = 2 + style.content_margin_bottom = 3 + return style + + +func _configure_index_focus() -> void: + var controls: Array[Control] = [] + for control: Control in _index_controls: + if control != null and is_instance_valid(control): + controls.append(control) + if not controls.is_empty(): + ControllerFocusNavigationType.configure_spatial_neighbors(controls) + + +func _apply_index_presentation() -> void: + if _index_body == null: + return + var scale_value := 0.82 if _compact_presentation else PAGE_CONTENT_SCALE + _index_body.scale = Vector2.ONE * scale_value + + func _apply_presentation_margins() -> void: if _outer_margin == null: return @@ -475,11 +810,6 @@ func _center_scaled_content(control: Control) -> void: func _configure_catalog_scroll_bar() -> void: - _catalog_scroll_snap_timer = Timer.new() - _catalog_scroll_snap_timer.one_shot = true - _catalog_scroll_snap_timer.wait_time = CATALOG_SNAP_DELAY - _catalog_scroll_snap_timer.timeout.connect(_snap_catalog_scroll_to_row) - _catalog_scroll.add_child(_catalog_scroll_snap_timer) var scroll_bar: VScrollBar = _catalog_scroll.get_v_scroll_bar() scroll_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE scroll_bar.focus_mode = Control.FOCUS_NONE @@ -525,25 +855,6 @@ func _make_scroll_indicator( func _on_catalog_scroll_value_changed(_value: float) -> void: - if _snapping_catalog_scroll: - return - _catalog_scroll_snap_timer.start() - _refresh_catalog_scroll_indicators() - - -func _snap_catalog_scroll_to_row() -> void: - var scroll_bar: VScrollBar = _catalog_scroll.get_v_scroll_bar() - var value: float = scroll_bar.value - var maximum_scroll: float = scroll_bar.max_value - scroll_bar.page - var snapped_value: float = clampf( - roundf(value / CATALOG_ROW_STEP) * CATALOG_ROW_STEP, - scroll_bar.min_value, - maximum_scroll, - ) - if not is_equal_approx(value, snapped_value): - _snapping_catalog_scroll = true - _catalog_scroll.scroll_vertical = roundi(snapped_value) - _snapping_catalog_scroll = false _refresh_catalog_scroll_indicators() @@ -575,11 +886,30 @@ func _refresh_catalog() -> void: var preserved_entry_key: StringName = _selected_entry_key _clear_entries() var species: Array[FishDataType] = [] + var category_total: int = 0 if _catalog != null: - species = LogbookCatalog.ordered_species(_catalog.candidates) + for fish: FishDataType in LogbookCatalog.ordered_species( + _catalog.candidates + ): + if LogbookCatalog.category_for(fish) == _category: + category_total += 1 + var available_now_resolver := Callable() + if _fishing_spot != null and is_instance_valid(_fishing_spot): + available_now_resolver = Callable( + _fishing_spot, + "is_species_available_now", + ) + species = LogbookCatalog.filtered_species( + _catalog.candidates, + _category, + _collection_log, + _inventory, + _index_options, + available_now_resolver, + _current_season(), + _current_is_night(), + ) for fish: FishDataType in species: - if LogbookCatalog.category_for(fish) != _category: - continue var discovered: bool = _collection_log.has_discovered(fish.id) var entry: Button = _make_entry(fish, discovered) var selection_key: StringName = ( @@ -599,7 +929,11 @@ func _refresh_catalog() -> void: ) ) _catalog_grid.add_child(entry) - _empty_state.text = LogbookCatalog.empty_state(_category) + _empty_state.text = ( + "No entries match this index." + if _index_has_active_criteria() + else LogbookCatalog.empty_state(_category) + ) _empty_state.visible = _entry_buttons.is_empty() _catalog_scroll.visible = not _entry_buttons.is_empty() if ( @@ -616,10 +950,269 @@ func _refresh_catalog() -> void: _selected_entry_key = StringName() _refresh_selection_styles() _refresh_details(false) + _update_index_controls(species.size(), category_total) set_interactive(_interactive) _refresh_catalog_scroll_indicators.call_deferred() +func _toggle_index_page() -> void: + _set_index_open(not _index_open) + + +func _set_index_open(open: bool, restore_focus: bool = true) -> void: + _index_open = open + if _index_tab != null: + _index_tab.set_selected(open) + _index_tab.tooltip_text = ( + "Return to catch record" if open else "Open catalog index" + ) + if _index_body != null: + _index_body.visible = open + if _detail_body != null: + _detail_body.visible = not open + if not open: + _refresh_details(false) + _update_index_controls() + if restore_focus and _active and _interactive: + if open: + _controller_zone = ControllerZone.INDEX + set_interactive(true) + _index_search.grab_focus() + elif _controller_zone == ControllerZone.INDEX: + _controller_zone = ControllerZone.TABS + set_interactive(true) + _index_tab.grab_focus() + + +func _on_index_search_changed(value: String) -> void: + if _index_update_in_progress: + return + _index_options["search"] = value + _refresh_catalog() + + +func _cycle_index_option(key: StringName, direction: int = 1) -> void: + if not _index_options.has(key): + return + if key == &"descending": + _index_options[key] = not bool(_index_options[key]) + elif key == &"group": + var groups: Array[String] = [""] + if _catalog != null: + groups.append_array(LogbookCatalog.available_groups( + _catalog.candidates, + _category, + _collection_log, + )) + var current_index := groups.find(str(_index_options[key])) + if current_index < 0: + current_index = 0 + _index_options[key] = groups[wrapi( + current_index + direction, + 0, + groups.size(), + )] + else: + var option_count := _index_option_count(key) + if option_count <= 0: + return + _index_options[key] = wrapi( + int(_index_options[key]) + direction, + 0, + option_count, + ) + _refresh_catalog() + + +func _on_index_option_gui_input( + event: InputEvent, + key: StringName, +) -> void: + var mouse_event := event as InputEventMouseButton + if mouse_event == null or not mouse_event.pressed: + return + if mouse_event.button_index in [MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_WHEEL_UP]: + _cycle_index_option(key, -1) + get_viewport().set_input_as_handled() + elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + _cycle_index_option(key, 1) + get_viewport().set_input_as_handled() + + +func _index_key_for_control(control: Control) -> StringName: + for key: StringName in _index_option_buttons: + if _index_option_buttons[key] == control: + return key + return StringName() + + +func _reset_index() -> void: + _index_options = { + "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": "", + } + _index_update_in_progress = true + _index_search.text = "" + _index_update_in_progress = false + _refresh_catalog() + + +func _update_index_controls( + visible_count: int = -1, + category_total: int = -1, +) -> void: + if visible_count >= 0: + _index_visible_count = visible_count + if category_total >= 0: + _index_category_total = category_total + if _index_summary != null: + _index_summary.text = "%d shown of %d" % [ + _index_visible_count, + _index_category_total, + ] + for key: StringName in _index_option_buttons: + var button := _index_option_buttons[key] + if button == null: + continue + button.text = _index_option_label(key) + button.accessibility_name = "%s: %s" % [ + String(key).replace("_", " "), + button.text, + ] + if _index_tab != null: + var marked := _index_has_active_criteria() + _index_tab.text = "index •" if marked else "index" + _index_tab.accessibility_name = ( + "Catalog index, filters active" if marked else "Catalog index" + ) + if _index_reset != null: + _index_reset.disabled = not _index_has_active_criteria() + + +func _index_option_label(key: StringName) -> String: + match key: + &"show": + return INDEX_SHOW_LABELS[int(_index_options[key])] + &"sort": + return INDEX_SORT_LABELS[int(_index_options[key])] + &"availability": + return INDEX_AVAILABILITY_LABELS[int(_index_options[key])] + &"season": + return INDEX_SEASON_LABELS[int(_index_options[key])] + &"time": + return INDEX_TIME_LABELS[int(_index_options[key])] + &"rarity": + return INDEX_RARITY_LABELS[int(_index_options[key])] + &"method": + return INDEX_METHOD_LABELS[int(_index_options[key])] + &"holding": + return INDEX_HOLDING_LABELS[int(_index_options[key])] + &"group": + var group := str(_index_options[key]) + return group if not group.is_empty() else "any group" + &"descending": + var descending := bool(_index_options[key]) + match int(_index_options["sort"]): + LogbookCatalog.SortMode.NAME: + return "z–a" if descending else "a–z" + LogbookCatalog.SortMode.CATALOG_NUMBER: + return "high–low" if descending else "low–high" + LogbookCatalog.SortMode.RARITY: + return ( + "rare–common" if descending else "common–rare" + ) + LogbookCatalog.SortMode.VALUE, LogbookCatalog.SortMode.WEIGHT: + return "high–low" if descending else "low–high" + _: + return "most first" if descending else "least first" + return "any" + + +func _index_option_count(key: StringName) -> int: + match key: + &"show": + return INDEX_SHOW_LABELS.size() + &"sort": + return INDEX_SORT_LABELS.size() + &"availability": + return INDEX_AVAILABILITY_LABELS.size() + &"season": + return INDEX_SEASON_LABELS.size() + &"time": + return INDEX_TIME_LABELS.size() + &"rarity": + return INDEX_RARITY_LABELS.size() + &"method": + return INDEX_METHOD_LABELS.size() + &"holding": + return INDEX_HOLDING_LABELS.size() + return 0 + + +func _index_has_active_criteria() -> bool: + return ( + not str(_index_options["search"]).is_empty() + or int(_index_options["show"]) + != LogbookCatalog.DiscoveryFilter.ALL + or int(_index_options["sort"]) + != LogbookCatalog.SortMode.CATALOG_NUMBER + or bool(_index_options["descending"]) + or int(_index_options["availability"]) + != LogbookCatalog.AvailabilityFilter.ANY + or int(_index_options["season"]) + != LogbookCatalog.SeasonFilter.ANY + or int(_index_options["time"]) + != LogbookCatalog.TimeFilter.ANY + or int(_index_options["rarity"]) + != LogbookCatalog.RarityFilter.ANY + or int(_index_options["method"]) + != LogbookCatalog.MethodFilter.ANY + or int(_index_options["holding"]) + != LogbookCatalog.HoldingFilter.ANY + or not str(_index_options["group"]).is_empty() + ) + + +func _current_season() -> int: + return ( + _world_time.get_season() + if _world_time != null + else CalendarSeason.UNKNOWN + ) + + +func _current_is_night() -> bool: + return _world_time != null and _world_time.is_night_period() + + +func notify_availability_context_changed() -> void: + _on_logbook_context_changed() + + +func _on_logbook_context_changed( + _first: Variant = null, + _second: Variant = null, +) -> void: + if ( + int(_index_options["availability"]) + == LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW + or int(_index_options["season"]) + == LogbookCatalog.SeasonFilter.CURRENT + or int(_index_options["time"]) + == LogbookCatalog.TimeFilter.CURRENT + ): + _refresh_catalog() + + func _make_entry(fish: FishDataType, discovered: bool) -> Button: var entry := Button.new() entry.custom_minimum_size = CATALOG_ENTRY_SIZE @@ -690,8 +1283,11 @@ func _add_entry_content( func _select_category(category: LogbookCatalog.Category) -> void: if category == _category: + if _index_open: + _set_index_open(false) return _category = category + _set_index_open(false, false) _selected_id = StringName() _selected_entry_key = StringName() for index: int in _category_tabs.size(): @@ -710,6 +1306,11 @@ func _configure_category_focus() -> void: tab.focus_neighbor_right = tab.get_path_to( _category_tabs[mini(index + 1, _category_tabs.size() - 1)] ) + if not _category_tabs.is_empty() and _index_tab != null: + var final_tab: Button = _category_tabs.back() + final_tab.focus_neighbor_right = final_tab.get_path_to(_index_tab) + _index_tab.focus_neighbor_left = _index_tab.get_path_to(final_tab) + _index_tab.focus_neighbor_right = _index_tab.get_path_to(_index_tab) func _animate_tab_entry() -> void: @@ -717,11 +1318,15 @@ func _animate_tab_entry() -> void: (_category_tabs[index] as OrganizerTabType).animate_entrance( float(index) * 0.025 ) + if _index_tab != null: + _index_tab.animate_entrance(float(_category_tabs.size()) * 0.025) func _settle_tabs_for_close() -> void: for tab: OrganizerTabType in _category_tabs: tab.settle_for_close() + if _index_tab != null: + _index_tab.settle_for_close() func _begin_category_transition() -> void: @@ -777,9 +1382,12 @@ func _set_category_content_alpha(alpha: float) -> void: _empty_state.modulate.a = alpha if _detail_body != null: _detail_body.modulate.a = alpha + if _index_body != null: + _index_body.modulate.a = alpha func _select_entry(entry_key: StringName, fish_id: StringName) -> void: + _set_index_open(false, false) if fish_id.is_empty(): _selected_id = StringName() _selected_entry_key = entry_key @@ -1311,7 +1919,9 @@ func _on_collection_changed() -> void: func _on_inventory_changed() -> void: - _refresh_details(false) + # Ownership filters and favorite-backed views depend on the live cooler, + # while rebuilding the catalog also keeps the selected detail current. + _refresh_catalog() func _unknown_selection_key(fish: FishDataType) -> StringName: diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 3c5dc77..95263f5 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -105,6 +105,7 @@ signal controller_hotbar_management_requested(initial_slot: int) signal controller_hotbar_management_ended signal menu_exit_started signal shop_cooler_modal_changed(is_open: bool) +signal controller_text_entry_requested(control: Control) enum Section { COOLER, @@ -409,6 +410,9 @@ func _ready() -> void: _logbook_tab.pressed.connect( _show_section.bind(Section.LOGBOOK) ) + _catalog_logbook.controller_text_entry_requested.connect( + controller_text_entry_requested.emit + ) _the_net_tab.pressed.connect(_show_section.bind(Section.NET)) _mail_tab.pressed.connect(_show_section.bind(Section.MAIL)) _profile_tab.pressed.connect(_show_section.bind(Section.PROFILE)) @@ -556,6 +560,7 @@ func setup( discovery: DiscoveryClient, player_jobs: PlayerJobService, world_time: WorldTimeService, + world_weather: WorldWeatherService, world_environment: WorldEnvironment, world_sun: DirectionalLight3D, ) -> void: @@ -589,7 +594,14 @@ func setup( world_sun, ) _players_page.setup(network_player_list, discovery) - _catalog_logbook.setup(collection_log, inventory, catalog) + _catalog_logbook.setup( + collection_log, + inventory, + catalog, + fishing_spot, + world_time, + world_weather, + ) _the_net_page.setup(player_jobs, world_time) _network_mail_service.unread_count_changed.connect( _on_mail_unread_count_changed @@ -1547,11 +1559,14 @@ func open_menu() -> void: _cancel_page_tween() _prior_movement_enabled = _player.is_movement_enabled() _prior_camera_input_enabled = _player.is_camera_input_enabled() - _prior_mouse_mode = Input.mouse_mode + _player.set_movement_enabled(false) + # Ending an active right-click camera drag restores its ordinary pointer + # mode. Snapshot after that handoff so this menu never restores the drag's + # transient captured mode when it closes. + _player.set_camera_input_enabled(false) + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) _control_snapshot_stored = true _mouse_snapshot_stored = true - _player.set_movement_enabled(false) - _player.set_camera_input_enabled(false) _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE visible = true @@ -1763,6 +1778,16 @@ func _apply_mouse_close_policy(reason: CloseReason) -> void: _mouse_snapshot_stored = false +func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode: + # Mouse capture belongs to an in-progress camera drag, never to the player + # menu itself. A held right button can be released while this menu owns + # input, so restoring CAPTURED here would leave the pointer hidden without + # an active drag to release it. + if mouse_mode == Input.MOUSE_MODE_CAPTURED: + return Input.MOUSE_MODE_VISIBLE + return mouse_mode + + func _on_bite_activated() -> void: if visible: close_menu(CloseReason.BITE_STARTED) @@ -2550,7 +2575,10 @@ func _on_tackle_button_gui_input( call_deferred("_release_pointer_focus", button) if ( mouse_event == null - or mouse_event.button_index != MOUSE_BUTTON_RIGHT + or mouse_event.button_index not in [ + MOUSE_BUTTON_LEFT, + MOUSE_BUTTON_RIGHT, + ] or not mouse_event.pressed ): return @@ -2779,6 +2807,7 @@ func _toggle_active_tackle() -> void: func _on_active_bait_changed(_item_id: StringName) -> void: _update_tackle_detail() + _catalog_logbook.notify_availability_context_changed() func _on_active_lure_changed(_item_id: StringName) -> void: @@ -3733,6 +3762,7 @@ func _refresh_all() -> void: func _on_bag_changed() -> void: _refresh_bag() _refresh_tackle_box() + _catalog_logbook.notify_availability_context_changed() func _on_hotbar_changed() -> void: diff --git a/ui/player_storage.gd b/ui/player_storage.gd index 25eb0d4..e5369a4 100644 --- a/ui/player_storage.gd +++ b/ui/player_storage.gd @@ -4,6 +4,7 @@ extends Control const INPUT_OWNER: StringName = &"player_storage" signal menu_visibility_changed(is_open: bool) +signal decorate_requested var _player: Player var _fishing_spot: FishingSpot @@ -14,6 +15,9 @@ var _storage_grid: GeneralInventoryGrid var _inventory_count: Label var _storage_count: Label var _feedback: Label +var _title: Label +var _decorate_button: Button +var _home_access: bool = false var _prior_movement_enabled: bool = true var _prior_camera_enabled: bool = true var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE @@ -75,20 +79,42 @@ func set_world_interaction(interaction: PlayerStorageInteraction) -> void: func open_storage() -> bool: + return _open_storage(false) + + +func open_storage_from_home(allow_decorating: bool = true) -> bool: + return _open_storage(true, allow_decorating) + + +func _open_storage( + home_access: bool, + allow_decorating: bool = false, +) -> bool: if ( visible or _player == null - or _interaction == null - or not _interaction.is_local_player_in_range() or _fishing_spot == null or not _fishing_spot.can_open_fishing_shop() + or ( + not home_access + and ( + _interaction == null + or not _interaction.is_local_player_in_range() + ) + ) ): return false + _home_access = home_access + _title.text = "home storage" if home_access else "private storage" + _decorate_button.visible = home_access and allow_decorating _prior_movement_enabled = _player.is_movement_enabled() _prior_camera_enabled = _player.is_camera_input_enabled() - _prior_mouse_mode = Input.mouse_mode _player.set_movement_enabled(false) + # End an in-progress right-click camera drag before taking the snapshot. + # A held button can be released while this page owns input, so captured mode + # is never a valid mouse mode to restore when the page closes. _player.set_camera_input_enabled(false) + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _feedback.text = "select an item to move it · drag to choose a slot" @@ -112,10 +138,13 @@ func close_storage(restore_controls: bool = true) -> void: _player.set_movement_enabled(_prior_movement_enabled) _player.set_camera_input_enabled(_prior_camera_enabled) Input.mouse_mode = _prior_mouse_mode + _home_access = false menu_visibility_changed.emit(false) func close_for_range_exit() -> void: + if _home_access: + return close_storage() @@ -127,6 +156,12 @@ func close_for_session_end() -> void: close_storage(false) +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 consume_escape() -> bool: if not visible: return false @@ -248,12 +283,20 @@ func _build_ui() -> void: var header := HBoxContainer.new() layout.add_child(header) - var title := Label.new() - title.text = "private storage" - title.add_theme_font_size_override("font_size", 32) - title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY) - title.size_flags_horizontal = Control.SIZE_EXPAND_FILL - header.add_child(title) + _title = Label.new() + _title.text = "private storage" + _title.add_theme_font_size_override("font_size", 32) + _title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY) + _title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header.add_child(_title) + _decorate_button = Button.new() + _decorate_button.text = "decorate home" + _decorate_button.tooltip_text = "Place decor inside your home" + _decorate_button.custom_minimum_size = Vector2(160.0, 48.0) + UtilityPageStyle.apply_ocean_button(_decorate_button) + _decorate_button.hide() + _decorate_button.pressed.connect(_request_decorating) + header.add_child(_decorate_button) var close := Button.new() close.text = "×" close.tooltip_text = "Close storage" @@ -285,6 +328,13 @@ func _build_ui() -> void: layout.add_child(_feedback) +func _request_decorating() -> void: + if not _home_access: + return + close_storage() + decorate_requested.emit() + + func _build_column(parent: HBoxContainer, title_text: String) -> Dictionary: var panel := PanelContainer.new() panel.custom_minimum_size = Vector2(550.0, 0.0) diff --git a/ui/rv_upgrade_shop.gd b/ui/rv_upgrade_shop.gd new file mode 100644 index 0000000..11c0a4d --- /dev/null +++ b/ui/rv_upgrade_shop.gd @@ -0,0 +1,330 @@ +class_name RVUpgradeShop +extends Control + +const INPUT_OWNER: StringName = &"rv_upgrade_shop" +const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") +const CurrencyPresentationType = preload("res://ui/currency_presentation.gd") + +signal menu_visibility_changed(is_open: bool) +signal menu_exit_started + +var _player: Player +var _wallet: PlayerWallet +var _fishing_spot: FishingSpot +var _interaction: RVUpgradeInteraction +var _home_state: PlayerHomeState +var _network_shop: NetworkShopService +var _wallet_amount: CurrencyAmount +var _price_amount: CurrencyAmount +var _home_label: Label +var _storage_label: Label +var _feedback: Label +var _upgrade_button: Button +var _close_button: Button +var _transaction_pending: bool = false +var _prior_movement_enabled: bool = true +var _prior_camera_enabled: bool = true +var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE + + +func _ready() -> void: + UtilityPageStyleType.apply_page(self) + _build_ui() + hide() + + +func setup( + player: Player, + wallet: PlayerWallet, + fishing_spot: FishingSpot, + interaction: RVUpgradeInteraction, + home_state: PlayerHomeState, + network_shop: NetworkShopService, +) -> void: + _player = player + _wallet = wallet + _fishing_spot = fishing_spot + _interaction = interaction + _home_state = home_state + _network_shop = network_shop + if not _wallet.balance_changed.is_connected(_on_wallet_changed): + _wallet.balance_changed.connect(_on_wallet_changed) + if not _home_state.changed.is_connected(_refresh): + _home_state.changed.connect(_refresh) + if ( + _network_shop != null + and not _network_shop.local_purchase_pending.is_connected( + _on_purchase_pending + ) + ): + _network_shop.local_purchase_pending.connect(_on_purchase_pending) + _network_shop.local_purchase_finished.connect(_on_purchase_finished) + _refresh() + + +func set_world_interaction(interaction: RVUpgradeInteraction) -> void: + _interaction = interaction + + +func open_shop() -> bool: + if ( + visible + or _player == null + or _wallet == null + or _fishing_spot == null + or _interaction == null + or not _interaction.is_local_player_in_range() + or not _fishing_spot.can_open_fishing_shop() + ): + return false + _prior_movement_enabled = _player.is_movement_enabled() + _prior_camera_enabled = _player.is_camera_input_enabled() + _player.set_movement_enabled(false) + _player.set_camera_input_enabled(false) + _prior_mouse_mode = _restorable_mouse_mode(Input.mouse_mode) + _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _transaction_pending = ( + _network_shop != null + and _network_shop.is_local_purchase_pending() + ) + _feedback.text = "" + _refresh() + show() + _upgrade_button.call_deferred("grab_focus") + menu_visibility_changed.emit(true) + return true + + +func close_shop(restore_controls: bool = true) -> void: + if not visible: + return + menu_exit_started.emit() + get_viewport().gui_release_focus() + hide() + if _fishing_spot != null: + _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false) + if restore_controls and _player != null: + _player.set_movement_enabled(_prior_movement_enabled) + _player.set_camera_input_enabled(_prior_camera_enabled) + Input.mouse_mode = _prior_mouse_mode + menu_visibility_changed.emit(false) + + +func consume_escape() -> bool: + if not visible: + return false + close_shop() + return true + + +func close_for_range_exit() -> void: + close_shop() + + +func close_for_water_recovery() -> void: + close_shop(false) + + +func close_for_session_end() -> void: + close_shop(false) + + +func _unhandled_input(event: InputEvent) -> void: + if not visible or not event.is_action_pressed("ui_cancel"): + return + close_shop() + get_viewport().set_input_as_handled() + + +func _purchase_upgrade() -> void: + if _network_shop == null or _transaction_pending: + return + var request_id: String = _network_shop.request_home_upgrade() + if request_id.is_empty(): + _refresh() + + +func _on_purchase_pending(_request_id: String) -> void: + _transaction_pending = true + _feedback.text = "upgrading…" + _refresh() + + +func _on_purchase_finished( + _request_id: String, + accepted: bool, + message: String, + product_id: StringName, + category: int, + _quantity: int, + _total_cost: int, +) -> void: + if ( + category != NetworkShopProtocol.ProductCategory.HOME_UPGRADE + or product_id != NetworkShopService.HOME_UPGRADE_PRODUCT_ID + ): + return + _transaction_pending = false + _feedback.text = "starter RV unlocked" if accepted else message.to_lower() + _refresh() + + +func _on_wallet_changed(_balance: int, _delta: int) -> void: + _refresh() + + +func _refresh() -> void: + if _wallet_amount == null: + return + var balance: int = _wallet.get_balance() if _wallet != null else 0 + var tier: int = _home_state.get_tier() if _home_state != null else 0 + var current_capacity: int = ( + _home_state.get_storage_capacity() if _home_state != null else 9 + ) + var next_capacity: int = ( + _home_state.get_next_storage_capacity() if _home_state != null else -1 + ) + var cost: int = ( + _home_state.get_next_upgrade_cost() if _home_state != null else -1 + ) + _wallet_amount.set_amount(balance) + _price_amount.set_amount(maxi(cost, 0)) + if tier >= PlayerHomeState.MAX_TIER: + _home_label.text = "starter RV" + _storage_label.text = "storage · %d slots" % current_capacity + _upgrade_button.text = "unlocked" + _upgrade_button.disabled = true + _price_amount.visible = false + else: + _home_label.text = "tent → starter RV" + _storage_label.text = "storage · %d → %d slots" % [ + current_capacity, next_capacity, + ] + _upgrade_button.text = "upgrade" + _upgrade_button.disabled = ( + _transaction_pending + or _wallet == null + or not _wallet.can_afford(cost) + or _network_shop == null + or not _network_shop.can_request_home_upgrade() + ) + _price_amount.visible = true + + +func _restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode: + return ( + Input.MOUSE_MODE_VISIBLE + if mouse_mode == Input.MOUSE_MODE_CAPTURED + else mouse_mode + ) + + +func _build_ui() -> void: + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + z_index = 83 + mouse_filter = Control.MOUSE_FILTER_STOP + var blocker := ColorRect.new() + blocker.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + blocker.color = Color(0.02, 0.08, 0.10, 0.55) + blocker.mouse_filter = Control.MOUSE_FILTER_STOP + add_child(blocker) + + var panel := PanelContainer.new() + panel.set_anchors_preset(Control.PRESET_CENTER) + panel.position = Vector2(-390.0, -225.0) + panel.size = Vector2(780.0, 450.0) + panel.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style( + UtilityPageStyleType.OCEAN_PANEL_DEEP, 24 + ), + ) + add_child(panel) + var margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + margin.add_theme_constant_override(side, 28) + panel.add_child(margin) + var layout := VBoxContainer.new() + layout.add_theme_constant_override("separation", 18) + margin.add_child(layout) + + var header := HBoxContainer.new() + header.add_theme_constant_override("separation", 12) + layout.add_child(header) + var title := Label.new() + title.text = "home upgrades" + title.add_theme_font_size_override("font_size", 38) + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header.add_child(title) + _wallet_amount = CurrencyPresentationType.instantiate_amount(0, 24.0) + header.add_child(_wallet_amount) + + var offer := PanelContainer.new() + offer.size_flags_vertical = Control.SIZE_EXPAND_FILL + offer.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style( + UtilityPageStyleType.OCEAN_FIELD, 18 + ), + ) + layout.add_child(offer) + var offer_margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + offer_margin.add_theme_constant_override(side, 28) + offer.add_child(offer_margin) + var details := VBoxContainer.new() + details.alignment = BoxContainer.ALIGNMENT_CENTER + details.add_theme_constant_override("separation", 14) + offer_margin.add_child(details) + _home_label = Label.new() + _home_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _home_label.add_theme_font_size_override("font_size", 34) + details.add_child(_home_label) + var description := Label.new() + description.text = ( + "A weatherproof home with a larger interior and built-in storage." + ) + description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + description.add_theme_font_size_override("font_size", 20) + details.add_child(description) + _storage_label = Label.new() + _storage_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _storage_label.add_theme_font_size_override("font_size", 22) + details.add_child(_storage_label) + var purchase_row := HBoxContainer.new() + purchase_row.alignment = BoxContainer.ALIGNMENT_CENTER + purchase_row.add_theme_constant_override("separation", 10) + details.add_child(purchase_row) + _upgrade_button = Button.new() + _upgrade_button.custom_minimum_size = Vector2(220.0, 60.0) + UtilityPageStyleType.apply_ocean_button(_upgrade_button) + _upgrade_button.pressed.connect(_purchase_upgrade) + purchase_row.add_child(_upgrade_button) + _price_amount = CurrencyPresentationType.instantiate_amount(0, 24.0) + purchase_row.add_child(_price_amount) + + var footer := HBoxContainer.new() + footer.add_theme_constant_override("separation", 12) + layout.add_child(footer) + _feedback = Label.new() + _feedback.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _feedback.add_theme_font_size_override("font_size", 18) + footer.add_child(_feedback) + _close_button = Button.new() + _close_button.text = "close" + _close_button.custom_minimum_size = Vector2(150.0, 50.0) + UtilityPageStyleType.apply_ocean_button(_close_button) + _close_button.pressed.connect(close_shop) + footer.add_child(_close_button) + _upgrade_button.focus_neighbor_bottom = _upgrade_button.get_path_to( + _close_button + ) + _close_button.focus_neighbor_top = _close_button.get_path_to( + _upgrade_button + ) diff --git a/ui/rv_upgrade_shop.gd.uid b/ui/rv_upgrade_shop.gd.uid new file mode 100644 index 0000000..ddd043e --- /dev/null +++ b/ui/rv_upgrade_shop.gd.uid @@ -0,0 +1 @@ +uid://b5pnsuph7fxrd diff --git a/ui/rv_upgrade_shop.tscn b/ui/rv_upgrade_shop.tscn new file mode 100644 index 0000000..22abf58 --- /dev/null +++ b/ui/rv_upgrade_shop.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/rv_upgrade_shop.gd" id="1_script"] + +[node name="RVUpgradeShop" type="Control"] +unique_name_in_owner = true +visible = false +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_script") diff --git a/ui/save_slots_page.gd b/ui/save_slots_page.gd index ba142f7..74e3a7f 100644 --- a/ui/save_slots_page.gd +++ b/ui/save_slots_page.gd @@ -439,7 +439,7 @@ func _choose_import_path() -> void: _import_dialog.access = FileDialog.ACCESS_FILESYSTEM _import_dialog.use_native_dialog = false _import_dialog.filters = PackedStringArray([ - "*.nfsave ; NETfishing progression archive", + "*.nfsave ; straywild progression archive", ]) _import_dialog.file_selected.connect(_import_file_selected) if _interface_fonts != null: @@ -479,7 +479,7 @@ func _choose_export_path() -> void: _export_dialog.access = FileDialog.ACCESS_FILESYSTEM _export_dialog.use_native_dialog = false _export_dialog.filters = PackedStringArray([ - "*.nfsave ; NETfishing progression archive", + "*.nfsave ; straywild progression archive", ]) _export_dialog.file_selected.connect(_export_file_selected) if _interface_fonts != null: @@ -716,7 +716,7 @@ func _safe_filename(value: String) -> String: var result: String = value.strip_edges().to_lower().replace(" ", "-") for character: String in ["/", "\\", ":", "*", "?", "\"", "<", ">", "|"]: result = result.replace(character, "") - return "netfishing-save" if result.is_empty() else result + return "straywild-save" if result.is_empty() else result func _on_slots_changed() -> void: diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index 049b9c9..7055d96 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -40,6 +40,7 @@ signal opened signal crisp_reset_focus_requested signal panel_visibility_changed(is_visible: bool) signal navigation_transition_started +signal data_root_changed enum PresentationMode { TITLE_EMBEDDED, @@ -612,6 +613,7 @@ func _refresh_data_page() -> void: %DataStorageMode.text = "storage mode: " + _data_root.storage_mode_text() %ChangeDataFolder.disabled = ( _data_root.override_active + or _presentation_mode != PresentationMode.TITLE_EMBEDDED or (_network_session != null and _network_session.is_session_active()) ) var fingerprint: String = ( @@ -668,6 +670,9 @@ func _choose_data_folder() -> void: if _data_root == null or _data_root.override_active: _feedback.text = "the data folder is externally managed." return + if _presentation_mode != PresentationMode.TITLE_EMBEDDED: + _feedback.text = "return to title to change the data folder." + return if _network_session != null and _network_session.is_session_active(): _feedback.text = "return to title to change the data folder." return @@ -684,6 +689,12 @@ func _choose_data_folder() -> void: func _change_data_folder(path: String) -> void: + if ( + _presentation_mode != PresentationMode.TITLE_EMBEDDED + or (_network_session != null and _network_session.is_session_active()) + ): + _feedback.text = "return to title to change the data folder." + return var result: Dictionary = PortableDataMigration.migrate_active_to( _data_root, path ) @@ -697,9 +708,7 @@ func _change_data_folder(path: String) -> void: ).to_lower() _refresh_data_page() return - _feedback.text = "data folder changed. NETfishing will close safely." - _refresh_data_page() - get_tree().call_deferred("quit") + _complete_data_root_change("data folder changed.") else: _feedback.text = str( result.get("message", "could not change the data folder.") @@ -708,17 +717,16 @@ func _change_data_folder(path: String) -> void: func _show_existing_data_folder_choice(path: String) -> void: var dialog := ConfirmationDialog.new() - dialog.title = "existing NETfishing data" + dialog.title = "existing straywild data" dialog.ok_button_text = "use selected data" dialog.dialog_text = ( - "the selected folder contains different NETfishing data.\n\n" + "the selected folder contains different straywild data.\n\n" + "choose one complete data set. data will not be merged." ) dialog.add_button("replace selected data", false, "replace") dialog.confirmed.connect(func() -> void: if _data_root.use_existing_root(path): - _feedback.text = "data folder changed. NETfishing will close safely." - get_tree().call_deferred("quit") + _complete_data_root_change("data folder changed.") else: _feedback.text = _data_root.error_message dialog.queue_free() @@ -733,7 +741,7 @@ func _show_existing_data_folder_choice(path: String) -> void: result.get("message", "could not replace selected data.") ) if bool(result.get("ok", false)): - get_tree().call_deferred("quit") + _complete_data_root_change(_feedback.text.to_lower()) dialog.queue_free() ) _interface_fonts.apply_utility_theme(dialog) @@ -744,6 +752,12 @@ func _show_existing_data_folder_choice(path: String) -> void: ) +func _complete_data_root_change(message: String) -> void: + _feedback.text = message + _refresh_data_page() + data_root_changed.emit() + + func _choose_identity_export(identity_type: String) -> void: if not _identity_operation_allowed(): return @@ -756,7 +770,7 @@ func _choose_identity_export(identity_type: String) -> void: _export_file_dialog.access = FileDialog.ACCESS_FILESYSTEM _export_file_dialog.use_native_dialog = false _export_file_dialog.filters = PackedStringArray([ - "*.nfidentity ; NETfishing identity backup", + "*.nfidentity ; straywild identity backup", ]) _export_file_dialog.file_selected.connect( _identity_export_file_selected @@ -786,7 +800,7 @@ func _choose_identity_import(identity_type: String) -> void: _backup_file_dialog.access = FileDialog.ACCESS_FILESYSTEM _backup_file_dialog.use_native_dialog = false _backup_file_dialog.filters = PackedStringArray([ - "*.nfidentity ; NETfishing identity backup", + "*.nfidentity ; straywild identity backup", ]) _backup_file_dialog.file_selected.connect( _identity_import_file_selected diff --git a/ui/shop_sell_inventory.gd b/ui/shop_sell_inventory.gd index 9e39850..f46bc6d 100644 --- a/ui/shop_sell_inventory.gd +++ b/ui/shop_sell_inventory.gd @@ -15,6 +15,7 @@ var _tray_grid: GridContainer var _total_label: Label var _feedback: Label var _sell_button: Button +var _all_button: Button var _staged: Dictionary[String, Dictionary] = {} @@ -135,6 +136,67 @@ func _on_remove_requested(key: String) -> void: _refresh_tray() +func _stage_all_eligible() -> void: + if _layout == null: + return + var added_count := 0 + for entry: Dictionary in _layout.get_entries( + PlayerInventoryLayout.InventoryContainer.INVENTORY + ): + var kind := int(entry.get("kind", -1)) + var identity := StringName(str(entry.get("identity", ""))) + if identity.is_empty(): + continue + var key := ( + PlayerInventoryLayout.catch_key(identity) + if kind == PlayerInventoryLayout.EntryKind.CATCH + else PlayerInventoryLayout.item_key(identity) + ) + if _staged.has(key): + continue + var record := _eligible_staged_record(kind, identity) + if record.is_empty(): + continue + _staged[key] = record + added_count += 1 + _feedback.text = ( + "" + if added_count > 0 + else "no additional items are eligible for sale" + ) + _refresh_tray() + + +func _eligible_staged_record(kind: int, identity: StringName) -> Dictionary: + if kind == PlayerInventoryLayout.EntryKind.CATCH: + var fish_catch := _fish_inventory.get_catch_by_id(identity) + if ( + fish_catch == null + or fish_catch.is_favorited + or ( + _reservations != null + and _reservations.is_fish_reserved(identity) + ) + ): + return {} + return {"kind": kind, "identity": identity, "quantity": 1} + if kind != PlayerInventoryLayout.EntryKind.ITEM: + return {} + var item := _item_catalog.get_item_by_id(identity) + if not ItemResalePolicy.is_sellable(item): + return {} + var available := _bag.get_quantity(identity) + if _reservations != null: + available = _reservations.get_available_item_quantity(identity) + if available < 1: + return {} + return { + "kind": kind, + "identity": identity, + "quantity": available, + } + + func _submit_sale() -> void: if _staged.is_empty() or _network_sale == null: return @@ -154,6 +216,7 @@ func _submit_sale() -> void: func _on_sale_pending(_request_id: String) -> void: _sell_button.disabled = true + _all_button.disabled = true _feedback.text = "selling…" @@ -243,6 +306,10 @@ func _refresh_tray() -> void: or _network_sale == null or _network_sale.is_local_sale_pending() ) + _all_button.disabled = ( + _network_sale != null + and _network_sale.is_local_sale_pending() + ) or not _has_unstaged_eligible_entries() call_deferred("_configure_focus") @@ -268,9 +335,32 @@ func _configure_focus() -> void: if control != null and control.focus_mode != Control.FOCUS_NONE: controls.append(control) controls.append(_sell_button) + controls.append(_all_button) ControllerFocusNavigation.configure_spatial_neighbors(controls) +func _has_unstaged_eligible_entries() -> bool: + if _layout == null: + return false + for entry: Dictionary in _layout.get_entries( + PlayerInventoryLayout.InventoryContainer.INVENTORY + ): + var kind := int(entry.get("kind", -1)) + var identity := StringName(str(entry.get("identity", ""))) + var key := ( + PlayerInventoryLayout.catch_key(identity) + if kind == PlayerInventoryLayout.EntryKind.CATCH + else PlayerInventoryLayout.item_key(identity) + ) + if ( + not identity.is_empty() + and not _staged.has(key) + and not _eligible_staged_record(kind, identity).is_empty() + ): + return true + return false + + func _calculate_total() -> int: var total: int = 0 for record: Dictionary in _staged.values(): @@ -347,9 +437,19 @@ func _build_ui() -> void: total_row.add_child(_total_label) _sell_button = Button.new() _sell_button.text = "sell" + _sell_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL UtilityPageStyle.apply_ocean_button(_sell_button) _sell_button.pressed.connect(_submit_sale) - (tray as VBoxContainer).add_child(_sell_button) + var sale_buttons := HBoxContainer.new() + sale_buttons.add_theme_constant_override("separation", 8) + (tray as VBoxContainer).add_child(sale_buttons) + sale_buttons.add_child(_sell_button) + _all_button = Button.new() + _all_button.text = "all" + _all_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + UtilityPageStyle.apply_ocean_button(_all_button) + _all_button.pressed.connect(_stage_all_eligible) + sale_buttons.add_child(_all_button) func _build_panel(parent: HBoxContainer, title_text: String) -> VBoxContainer: diff --git a/ui/title_credits_page.tscn b/ui/title_credits_page.tscn index 1fe54e0..2697ced 100644 --- a/ui/title_credits_page.tscn +++ b/ui/title_credits_page.tscn @@ -45,7 +45,7 @@ horizontal_alignment = 1 layout_mode = 2 theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1) theme_override_font_sizes/font_size = 18 -text = "NETfishing is created by Woofmeow" +text = "straywild is created by Woofmeow" horizontal_alignment = 1 [node name="Columns" type="HBoxContainer" parent="Paper/Margin/Layout"] diff --git a/ui/title_screen.gd b/ui/title_screen.gd index 07a61fd..0a80f63 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -31,55 +31,18 @@ const SaveSlotsPageType = preload("res://ui/save_slots_page.gd") const CurrencyPresentationType = preload( "res://ui/currency_presentation.gd" ) -const DECORATIVE_FISH_TEXTURES: Array[Texture2D] = [ - preload("res://fish/species/bass/fish_bass_striped.png"), - preload("res://fish/species/bluegill/fish_bluegill.png"), - preload("res://fish/species/carp/fish_carp_common.png"), - preload("res://fish/species/sunfish/fish_sunfish.png"), -] -const DECORATIVE_BUBBLE_TEXTURES: Array[Texture2D] = [ - preload("res://ui/assets/title/bubbles/bubble1.png"), - preload("res://ui/assets/title/bubbles/bubble2.png"), - preload("res://ui/assets/title/bubbles/bubble3.png"), -] - -const FIRST_FISH_DELAY_MIN: float = 2.0 -const FIRST_FISH_DELAY_MAX: float = 5.0 -const NEXT_FISH_DELAY_MIN: float = 5.0 -const NEXT_FISH_DELAY_MAX: float = 12.0 -const CROSSING_DURATION_MIN: float = 8.0 -const CROSSING_DURATION_MAX: float = 16.0 -const FISH_LONGEST_SIDE_MIN: float = 120.0 -const FISH_LONGEST_SIDE_MAX: float = 210.0 -const FISH_EDGE_MARGIN: float = 24.0 -const BUBBLE_EVENT_DELAY_MIN: float = 4.0 -const BUBBLE_EVENT_DELAY_MAX: float = 10.0 -const BUBBLE_CLUSTER_DELAY_MIN: float = 0.25 -const BUBBLE_CLUSTER_DELAY_MAX: float = 0.65 -const BUBBLE_TRAVEL_DURATION_MIN: float = 7.0 -const BUBBLE_TRAVEL_DURATION_MAX: float = 14.0 -const BUBBLE_SCALE_MIN: float = 0.65 -const BUBBLE_SCALE_MAX: float = 1.15 -const BUBBLE_OPACITY_MIN: float = 0.55 -const BUBBLE_OPACITY_MAX: float = 0.90 -const BUBBLE_DRIFT_MIN: float = 10.0 -const BUBBLE_DRIFT_MAX: float = 45.0 -const BUBBLE_WOBBLE_MIN: float = 3.0 -const BUBBLE_WOBBLE_MAX: float = 9.0 -const BUBBLE_EDGE_MARGIN: float = 16.0 -const MAX_DECORATIVE_BUBBLES: int = 6 -const MAX_TRANSITION_BUBBLES: int = 30 -const LOGO_ASPECT_RATIO: float = 2560.0 / 760.0 +const LOGO_ASPECT_RATIO: float = 2560.0 / 480.0 const LOGO_WIDTH_FACTOR: float = 0.4784 const LOGO_MIN_WIDTH: float = 294.0 const LOGO_MAX_WIDTH: float = 662.0 const TITLE_HORIZONTAL_MARGIN: float = 48.0 const TITLE_DESKTOP_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) const TITLE_COMPACT_REFERENCE_SIZE: Vector2 = Vector2(640.0, 480.0) -const BUBBLE_FIELD_MAX_WIDTH: float = 440.0 -const BUBBLE_FIELD_DESKTOP_HEIGHT: float = 318.0 +const BUBBLE_FIELD_MAX_WIDTH: float = 500.0 +const BUBBLE_FIELD_DESKTOP_HEIGHT: float = 360.0 const BUBBLE_FIELD_COMPACT_WIDTH: float = 294.0 const BUBBLE_FIELD_COMPACT_HEIGHT: float = 200.0 +const BUBBLE_FIELD_DESKTOP_OFFSET_Y: float = -10.0 const BUBBLE_COMPACT_HEIGHT_THRESHOLD: float = 560.0 const START_PROMPT_MIN_SCALE: float = 0.985 const START_PROMPT_MAX_SCALE: float = 1.015 @@ -87,6 +50,11 @@ const START_PROMPT_CYCLE_SECONDS: float = 3.0 const QUICK_MENU_ENTER_DURATION: float = 0.14 const QUICK_MENU_EXIT_DURATION: float = 0.10 const QUICK_MENU_ENTER_SCALE: float = 0.97 +const INTRO_PROMPT_FADE_DURATION: float = 0.24 +const INTRO_LOGO_FLOAT_DURATION: float = 0.72 +const INTRO_MENU_REVEAL_DELAY: float = 0.34 +const INTRO_MENU_REVEAL_DURATION: float = 0.30 +const INTRO_LOGO_CENTER_Y_RATIO: float = 0.43 signal new_game_requested(world_layout: StringName, world_seed: int) signal continue_game_requested @@ -113,6 +81,7 @@ enum ConfirmationAction { @onready var _quit_button: BubbleButtonType = %QuitButton @onready var _join_game_button: BubbleButtonType = %JoinGameButton @onready var _feedback_label: RichTextLabel = %FeedbackLabel +@onready var _continue_stats_drawer: RichTextLabel = %ContinueStatsDrawer @onready var _confirmation_page: TitleConfirmationBubblePageType = ( %ConfirmationPage ) @@ -121,11 +90,6 @@ enum ConfirmationAction { %TitlePresentationScaleRoot ) @onready var _background: ColorRect = %Background -@onready var _decorative_fish_layer: Control = %DecorativeFishLayer -@onready var _decorative_fish_timer: Timer = %DecorativeFishTimer -@onready var _decorative_bubble_layer: Control = %DecorativeBubbleLayer -@onready var _decorative_bubble_event_timer: Timer = %DecorativeBubbleEventTimer -@onready var _decorative_bubble_cluster_timer: Timer = %DecorativeBubbleClusterTimer @onready var _title_logo: TextureRect = %TitleLogo @onready var _playtest_label: Label = %PlaytestLabel @onready var _presentation_center: CenterContainer = %Center @@ -149,17 +113,6 @@ var _settings_manager: SettingsManagerType var _inspection: SaveInspectionType var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE var _action_in_progress: bool = false -var _decorative_rng := RandomNumberGenerator.new() -var _decorative_fish: TextureRect -var _decorative_fish_tween: Tween -var _decorative_presentation_ready: bool = false -var _decorative_presentation_active: bool = false -var _decorative_generation: int = 0 -var _decorative_bubbles: Array[TextureRect] = [] -var _decorative_bubble_tweens: Dictionary[int, Tween] = {} -var _transition_bubble_ids: Dictionary[int, bool] = {} -var _pending_cluster_bubbles: int = 0 -var _minimal_presentation: bool = false var _awaiting_start_input: bool = false var _start_prompt_elapsed: float = 0.0 var _navigation_focus_active: bool = false @@ -219,27 +172,16 @@ func _ready() -> void: _settings_panel.closed.connect(_on_settings_closed) _settings_panel.closing.connect(_on_settings_closing) _settings_panel.opened.connect(_on_settings_opened) - _settings_panel.navigation_transition_started.connect( - _emit_navigation_bubble_flurry - ) _credits_page.back_requested.connect(_close_credits) _save_slots_page.back_requested.connect(_close_save_slots) _save_slots_page.play_requested.connect(_on_slot_play_requested) _save_slots_page.create_requested.connect(_on_new_slot_requested) _bubble_field.configure(_get_title_buttons()) - _bubble_field.motion_scale = 0.0 - _decorative_fish_timer.timeout.connect(_on_decorative_fish_timer_timeout) - _decorative_bubble_event_timer.timeout.connect( - _on_decorative_bubble_event_timer_timeout - ) - _decorative_bubble_cluster_timer.timeout.connect( - _on_decorative_bubble_cluster_timer_timeout - ) + _bubble_field.motion_scale = 1.0 visibility_changed.connect(_on_title_visibility_changed) resized.connect(_update_responsive_title_stage) get_window().size_changed.connect(_update_responsive_title_stage) _start_prompt_label.resized.connect(_update_start_prompt_pivot) - _decorative_rng.randomize() set_process(false) call_deferred("_update_responsive_title_stage") call_deferred("_update_start_prompt_pivot") @@ -269,19 +211,9 @@ func setup( Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _refresh_save_inspection() show() - _decorative_presentation_ready = true - _start_decorative_presentation() _begin_title_entry() -func set_minimal_presentation(enabled: bool) -> void: - _minimal_presentation = enabled - if _minimal_presentation: - _stop_decorative_presentation() - elif visible: - _start_decorative_presentation() - - func reopen() -> void: _cancel_title_entry_transition() _cancel_title_settings_transition() @@ -295,7 +227,6 @@ func reopen() -> void: _save_slots_page.close_page() _refresh_save_inspection() show() - _start_decorative_presentation() _start_entry_prompt_animation() @@ -312,9 +243,16 @@ func reopen_to_menu() -> void: _set_title_bubbles_interactive(true) +func refresh_after_data_root_change() -> void: + _save_slots_page.refresh_page() + _refresh_save_inspection() + + func open_join_game_page(endpoint: String = "") -> void: _cancel_title_entry_transition() _awaiting_start_input = false + _set_title_bubbles_interactive(false) + _release_primary_menu_focus() _start_prompt_center.hide() _presentation_center.hide() _settings_panel.hide() @@ -353,6 +291,7 @@ func _close_join_game() -> void: _presentation_center.show() _button_center.show() _start_prompt_center.hide() + _set_title_bubbles_interactive(true) _focus_initial_button() @@ -395,28 +334,6 @@ func is_awaiting_start_input() -> bool: return _awaiting_start_input -func is_decorative_presentation_active() -> bool: - return _decorative_presentation_active - - -func get_active_decorative_fish_count() -> int: - return 1 if is_instance_valid(_decorative_fish) else 0 - - -func get_active_decorative_bubble_count() -> int: - return _decorative_bubbles.size() - - -func is_decorative_bubble_scheduler_active() -> bool: - return ( - _decorative_presentation_active - and ( - not _decorative_bubble_event_timer.is_stopped() - or not _decorative_bubble_cluster_timer.is_stopped() - ) - ) - - func _update_title_layout() -> void: if not is_node_ready(): return @@ -464,6 +381,12 @@ func _update_title_layout() -> void: Vector2(field_width, field_height), compact_layout ) + _bubble_field.position = Vector2( + 0.0, + BUBBLE_FIELD_DESKTOP_OFFSET_Y + * field_height + / BUBBLE_FIELD_DESKTOP_HEIGHT, + ) _configure_title_controller_navigation() if not _title_settings_transition_active: call_deferred("_capture_title_bubble_rest_position") @@ -520,12 +443,6 @@ func _update_world_preview_resolution() -> void: _world_pixel_size, displayed_size ) - var shader_material := _background.material as ShaderMaterial - if shader_material != null: - shader_material.set_shader_parameter( - "virtual_pixel_density", - Vector2(grid_size) - ) var logo_material := _title_logo.material as ShaderMaterial if logo_material != null: var render_scale: float = ( @@ -542,23 +459,6 @@ func _update_world_preview_resolution() -> void: ) -func _snap_world_preview(value: Vector2) -> Vector2: - var displayed_size := Vector2i( - maxi(1, roundi(size.x)), - maxi(1, roundi(size.y)) - ) - var grid_size: Vector2i = PlayerSettings.get_world_grid_size( - _world_pixel_size, - displayed_size - ) - var render_scale: float = float(grid_size.y) / float(displayed_size.y) - var step: float = 1.0 / render_scale - return Vector2( - roundf(value.x / step) * step, - roundf(value.y / step) * step - ) - - func _process(delta: float) -> void: if not visible: return @@ -725,6 +625,9 @@ func _prepare_awaiting_start_input() -> void: _continue_stats_hovered = false _continue_stats_focused = false _cancel_continue_stats_fade() + _continue_stats_drawer.hide() + _continue_stats_drawer.modulate.a = 0.0 + _continue_stats_drawer.scale = Vector2(1.0, 0.001) _main_content.show() _start_prompt_center.show() _start_prompt_center.modulate.a = 1.0 @@ -775,7 +678,6 @@ func _reveal_primary_menu() -> void: _navigation_focus_active = false _release_title_focus() _bubble_motion_root.position = Vector2.ZERO - _branding_motion_root.position = Vector2.ZERO _bubble_field.pivot_offset = _bubble_field.size * 0.5 _bubble_field.scale = Vector2.ONE * QUICK_MENU_ENTER_SCALE _bubble_field.modulate.a = 0.0 @@ -784,20 +686,28 @@ func _reveal_primary_menu() -> void: _start_prompt_center, "modulate:a", 0.0, - QUICK_MENU_EXIT_DURATION + INTRO_PROMPT_FADE_DURATION ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) + _title_entry_transition.tween_property( + _branding_motion_root, + "position", + Vector2.ZERO, + INTRO_LOGO_FLOAT_DURATION + ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) _title_entry_transition.tween_property( _bubble_field, "scale", Vector2.ONE, - QUICK_MENU_ENTER_DURATION - ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + INTRO_MENU_REVEAL_DURATION + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT).set_delay( + INTRO_MENU_REVEAL_DELAY + ) _title_entry_transition.tween_property( _bubble_field, "modulate:a", 1.0, - QUICK_MENU_ENTER_DURATION - ) + INTRO_MENU_REVEAL_DURATION + ).set_delay(INTRO_MENU_REVEAL_DELAY) _title_entry_transition.finished.connect( _finish_primary_menu_reveal.bind(generation), CONNECT_ONE_SHOT @@ -814,6 +724,7 @@ func _finish_primary_menu_reveal(generation: int) -> void: _title_entry_transition_active = false _start_prompt_center.hide() _start_prompt_center.modulate.a = 1.0 + _branding_motion_root.position = Vector2.ZERO _bubble_field.scale = Vector2.ONE _bubble_field.modulate.a = 1.0 _feedback_label.modulate.a = 0.0 @@ -855,10 +766,18 @@ func _prepare_intro_presentation(generation: int) -> void: return _branding_motion_root.size = _branding_slot.size _bubble_motion_root.size = _bubble_layout_slot.size - _branding_motion_root.position = Vector2.ZERO _bubble_motion_root.position = Vector2.ZERO _title_content_rest_position = _presentation_center.position _title_bubble_rest_position = _bubble_field.position + var branding_rect: Rect2 = _get_title_stage_rect(_branding_slot) + var desired_branding_center_y: float = ( + _title_presentation_scale_root.size.y + * INTRO_LOGO_CENTER_Y_RATIO + ) + _branding_motion_root.position = Vector2( + 0.0, + desired_branding_center_y - branding_rect.get_center().y, + ) _intro_geometry_ready = true @@ -908,7 +827,7 @@ func _update_continue_stats_visibility() -> void: and _presentation_center.visible ) if requested_visible: - _feedback_label.text = _get_continue_stats_text() + _continue_stats_drawer.text = _get_continue_stats_text() _fade_continue_stats_to(1.0 if requested_visible else 0.0) @@ -922,16 +841,47 @@ func _fade_continue_stats_to(target_opacity: float) -> void: if not is_node_ready(): return _cancel_continue_stats_fade() - if is_equal_approx(_feedback_label.modulate.a, target_opacity): - _feedback_label.modulate.a = target_opacity + var target_scale := Vector2( + 1.0, + 1.0 if target_opacity > 0.0 else 0.001, + ) + if target_opacity > 0.0: + _continue_stats_drawer.show() + if ( + is_equal_approx( + _continue_stats_drawer.modulate.a, + target_opacity, + ) + and _continue_stats_drawer.scale.is_equal_approx(target_scale) + ): + _continue_stats_drawer.modulate.a = target_opacity + _continue_stats_drawer.scale = target_scale + if target_opacity <= 0.0: + _continue_stats_drawer.hide() return - _continue_stats_fade = create_tween() + _continue_stats_fade = create_tween().set_parallel(true) _continue_stats_fade.tween_property( - _feedback_label, + _continue_stats_drawer, "modulate:a", target_opacity, QUICK_MENU_ENTER_DURATION ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + _continue_stats_fade.tween_property( + _continue_stats_drawer, + "scale", + target_scale, + QUICK_MENU_ENTER_DURATION + ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + if target_opacity <= 0.0: + _continue_stats_fade.chain().tween_callback( + _finish_continue_stats_hide + ) + + +func _finish_continue_stats_hide() -> void: + _continue_stats_fade = null + if _continue_stats_drawer.modulate.a <= 0.001: + _continue_stats_drawer.hide() func _cancel_continue_stats_fade() -> void: @@ -1180,7 +1130,6 @@ func _begin_confirmation_open() -> void: _confirmation_transition_generation += 1 _cancel_confirmation_transition() _confirmation_transition_active = true - _emit_navigation_bubble_flurry() _confirmation_title_content_rest_position = _presentation_center.position _set_confirmation_stage_rect() _confirmation_page.hide_page() @@ -1238,7 +1187,6 @@ func _begin_confirmation_return() -> void: _confirmation_transition_generation += 1 _cancel_confirmation_transition() _confirmation_transition_active = true - _emit_navigation_bubble_flurry() _confirmation_page.lock_interaction() var generation: int = _confirmation_transition_generation _confirmation_page.transition_out( @@ -1375,7 +1323,6 @@ func _begin_title_cluster_exit() -> void: _title_settings_transition_generation += 1 _cancel_title_settings_tween() _title_settings_transition_active = true - _emit_navigation_bubble_flurry() _title_content_rest_position = _presentation_center.position _title_bubble_rest_position = _bubble_field.position _set_title_bubbles_interactive(false) @@ -1475,6 +1422,8 @@ func _finish_title_cluster_return(generation: int) -> void: func _set_title_bubbles_interactive(interactive: bool) -> void: for bubble: BubbleButton in _get_title_buttons(): + if not interactive: + bubble.reset_hover_presentation() bubble.focus_mode = ( Control.FOCUS_ALL if interactive else Control.FOCUS_NONE ) @@ -1491,21 +1440,39 @@ func _configure_title_controller_navigation() -> void: ControllerFocusNavigationType.configure_spatial_neighbors( _get_title_buttons() ) - # The large Play bubble overlaps the inward edge of every surrounding - # bubble. Center-only spatial scoring otherwise links the four outside - # bubbles into a ring with no route back into Play. - _join_game_button.focus_neighbor_right = ( + _play_button.focus_neighbor_right = ( + _play_button.get_path_to(_join_game_button) + ) + _join_game_button.focus_neighbor_left = ( _join_game_button.get_path_to(_play_button) ) + _join_game_button.focus_neighbor_bottom = ( + _join_game_button.get_path_to(_settings_button) + ) _settings_button.focus_neighbor_left = ( _settings_button.get_path_to(_play_button) ) - _credits_button.focus_neighbor_right = ( + _settings_button.focus_neighbor_top = ( + _settings_button.get_path_to(_join_game_button) + ) + _settings_button.focus_neighbor_bottom = ( + _settings_button.get_path_to(_credits_button) + ) + _credits_button.focus_neighbor_left = ( _credits_button.get_path_to(_play_button) ) + _credits_button.focus_neighbor_top = ( + _credits_button.get_path_to(_settings_button) + ) + _credits_button.focus_neighbor_bottom = ( + _credits_button.get_path_to(_quit_button) + ) _quit_button.focus_neighbor_left = ( _quit_button.get_path_to(_play_button) ) + _quit_button.focus_neighbor_top = ( + _quit_button.get_path_to(_credits_button) + ) func _capture_title_bubble_rest_position() -> void: @@ -1575,7 +1542,7 @@ func _refresh_save_inspection() -> void: _delete_button.disabled = not _inspection.can_delete() _feedback_label.text = _center_feedback_text(_inspection.message) if _inspection.status == SaveInspectionType.Status.VALID_SUPPORTED: - _feedback_label.text = _get_continue_stats_text() + _continue_stats_drawer.text = _get_continue_stats_text() if not _inspection.can_continue(): _hide_continue_stats_context() else: @@ -1609,11 +1576,8 @@ func _on_quit_pressed() -> void: func _on_title_visibility_changed() -> void: - if not _decorative_presentation_ready: - return if visible: set_process(true) - _start_decorative_presentation() else: _hide_continue_stats_context() _stop_entry_prompt_animation() @@ -1621,482 +1585,3 @@ func _on_title_visibility_changed() -> void: _modal_restore_navigation_focus = false _reset_confirmation() _credits_page.close_page() - _stop_decorative_presentation() - - -func _start_decorative_presentation() -> void: - if ( - not _decorative_presentation_ready - or not visible - or _decorative_presentation_active - or _minimal_presentation - ): - return - _decorative_generation += 1 - _decorative_presentation_active = true - _schedule_next_decorative_fish( - FIRST_FISH_DELAY_MIN, - FIRST_FISH_DELAY_MAX - ) - _schedule_next_decorative_bubble_event( - BUBBLE_EVENT_DELAY_MIN, - BUBBLE_EVENT_DELAY_MAX - ) - - -func _stop_decorative_presentation() -> void: - _decorative_generation += 1 - _decorative_presentation_active = false - _decorative_fish_timer.stop() - if _decorative_fish_tween != null: - _decorative_fish_tween.kill() - _decorative_fish_tween = null - if is_instance_valid(_decorative_fish): - _decorative_fish.queue_free() - _decorative_fish = null - _decorative_bubble_event_timer.stop() - _decorative_bubble_cluster_timer.stop() - _pending_cluster_bubbles = 0 - for bubble_tween: Tween in _decorative_bubble_tweens.values(): - if bubble_tween != null: - bubble_tween.kill() - _decorative_bubble_tweens.clear() - for bubble: TextureRect in _decorative_bubbles: - if is_instance_valid(bubble): - bubble.queue_free() - _decorative_bubbles.clear() - _transition_bubble_ids.clear() - - -func _schedule_next_decorative_fish( - minimum_delay: float, - maximum_delay: float, -) -> void: - if not _decorative_presentation_active or not visible: - return - _decorative_fish_timer.start( - _decorative_rng.randf_range(minimum_delay, maximum_delay) - ) - - -func _on_decorative_fish_timer_timeout() -> void: - if ( - not _decorative_presentation_active - or not visible - or is_instance_valid(_decorative_fish) - ): - return - _spawn_decorative_fish(_decorative_generation) - - -func _spawn_decorative_fish(generation: int) -> void: - if ( - generation != _decorative_generation - or not _decorative_presentation_active - or not visible - or DECORATIVE_FISH_TEXTURES.is_empty() - ): - return - var layer_size: Vector2 = _decorative_fish_layer.size - if layer_size.x <= 1.0 or layer_size.y <= 1.0: - _schedule_next_decorative_fish(0.5, 1.0) - return - var texture: Texture2D = DECORATIVE_FISH_TEXTURES[ - _decorative_rng.randi_range(0, DECORATIVE_FISH_TEXTURES.size() - 1) - ] - var source_size: Vector2 = texture.get_size() - if source_size.x <= 0.0 or source_size.y <= 0.0: - _schedule_next_decorative_fish( - NEXT_FISH_DELAY_MIN, - NEXT_FISH_DELAY_MAX - ) - return - var longest_side: float = _decorative_rng.randf_range( - FISH_LONGEST_SIDE_MIN, - FISH_LONGEST_SIDE_MAX - ) - var presentation_scale: float = longest_side / maxf( - source_size.x, - source_size.y - ) - var presentation_size: Vector2 = source_size * presentation_scale - var fish_control := TextureRect.new() - fish_control.name = "DecorativeFish" - fish_control.texture = texture - fish_control.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - fish_control.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - fish_control.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - fish_control.mouse_filter = Control.MOUSE_FILTER_IGNORE - fish_control.custom_minimum_size = presentation_size - fish_control.size = presentation_size - fish_control.modulate.a = 0.46 - - var direction: float = ( - 1.0 - if _decorative_rng.randi_range(0, 1) == 1 - else -1.0 - ) - fish_control.flip_h = direction > 0.0 - var minimum_y: float = maxf(24.0, layer_size.y * 0.08) - var maximum_y: float = maxf( - minimum_y, - layer_size.y - presentation_size.y - maxf(24.0, layer_size.y * 0.08) - ) - var base_y: float = _decorative_rng.randf_range(minimum_y, maximum_y) - var bob_amplitude: float = _decorative_rng.randf_range(3.0, 8.0) - var bob_speed: float = _decorative_rng.randf_range(0.65, 1.15) - var crossing_duration: float = _decorative_rng.randf_range( - CROSSING_DURATION_MIN, - CROSSING_DURATION_MAX - ) - var horizontal_bounds: Vector2 = get_decorative_fish_crossing_bounds( - layer_size.x, - presentation_size.x, - direction, - ) - fish_control.position = Vector2(horizontal_bounds.x, base_y) - _decorative_fish_layer.add_child(fish_control) - _decorative_fish = fish_control - _decorative_fish_tween = create_tween() - _decorative_fish_tween.tween_method( - _update_decorative_fish.bind( - fish_control, - direction, - base_y, - bob_amplitude, - bob_speed, - crossing_duration, - generation - ), - 0.0, - 1.0, - crossing_duration - ) - _decorative_fish_tween.finished.connect( - _on_decorative_fish_finished.bind(fish_control, generation), - CONNECT_ONE_SHOT - ) - - -func _update_decorative_fish( - progress: float, - fish_control: TextureRect, - direction: float, - base_y: float, - bob_amplitude: float, - bob_speed: float, - crossing_duration: float, - generation: int, -) -> void: - if ( - generation != _decorative_generation - or not _decorative_presentation_active - or not is_instance_valid(fish_control) - ): - return - var horizontal_bounds: Vector2 = get_decorative_fish_crossing_bounds( - _decorative_fish_layer.size.x, - fish_control.size.x, - direction, - ) - fish_control.position.x = lerpf( - horizontal_bounds.x, horizontal_bounds.y, progress - ) - var bobbed_y: float = ( - base_y - + sin(progress * crossing_duration * bob_speed) * bob_amplitude - ) - fish_control.position.y = clampf( - bobbed_y, - 8.0, - maxf(8.0, _decorative_fish_layer.size.y - fish_control.size.y - 8.0) - ) - fish_control.position = _snap_world_preview(fish_control.position) - - -static func get_decorative_fish_crossing_bounds( - layer_width: float, - fish_width: float, - direction: float, -) -> Vector2: - var outside_left: float = -fish_width - FISH_EDGE_MARGIN - var outside_right: float = layer_width + FISH_EDGE_MARGIN - return ( - Vector2(outside_left, outside_right) - if direction > 0.0 - else Vector2(outside_right, outside_left) - ) - - -func _on_decorative_fish_finished( - fish_control: TextureRect, - generation: int, -) -> void: - if generation != _decorative_generation: - return - _decorative_fish_tween = null - if is_instance_valid(fish_control): - fish_control.queue_free() - if _decorative_fish == fish_control: - _decorative_fish = null - _schedule_next_decorative_fish( - NEXT_FISH_DELAY_MIN, - NEXT_FISH_DELAY_MAX - ) - - -func _schedule_next_decorative_bubble_event( - minimum_delay: float, - maximum_delay: float, -) -> void: - if not _decorative_presentation_active or not visible: - return - _decorative_bubble_event_timer.start( - _decorative_rng.randf_range(minimum_delay, maximum_delay) - ) - - -func _on_decorative_bubble_event_timer_timeout() -> void: - if not _decorative_presentation_active or not visible: - return - var available_slots: int = ( - MAX_DECORATIVE_BUBBLES - _get_idle_decorative_bubble_count() - ) - if available_slots <= 0: - _schedule_next_decorative_bubble_event( - BUBBLE_EVENT_DELAY_MIN, - BUBBLE_EVENT_DELAY_MAX - ) - return - var event_size: int = _choose_decorative_bubble_event_size() - event_size = mini(event_size, available_slots) - if not _spawn_decorative_bubble(_decorative_generation): - _schedule_next_decorative_bubble_event(0.5, 1.0) - return - _pending_cluster_bubbles = event_size - 1 - if _pending_cluster_bubbles > 0: - _start_decorative_bubble_cluster_timer() - else: - _schedule_next_decorative_bubble_event( - BUBBLE_EVENT_DELAY_MIN, - BUBBLE_EVENT_DELAY_MAX - ) - - -func _choose_decorative_bubble_event_size() -> int: - var roll: float = _decorative_rng.randf() - if roll < 0.75: - return 1 - if roll < 0.95: - return 2 - return 3 - - -func _start_decorative_bubble_cluster_timer() -> void: - _decorative_bubble_cluster_timer.start( - _decorative_rng.randf_range( - BUBBLE_CLUSTER_DELAY_MIN, - BUBBLE_CLUSTER_DELAY_MAX - ) - ) - - -func _on_decorative_bubble_cluster_timer_timeout() -> void: - if ( - not _decorative_presentation_active - or not visible - or _pending_cluster_bubbles <= 0 - ): - _pending_cluster_bubbles = 0 - return - if _get_idle_decorative_bubble_count() < MAX_DECORATIVE_BUBBLES: - _spawn_decorative_bubble(_decorative_generation) - _pending_cluster_bubbles -= 1 - if _pending_cluster_bubbles > 0: - _start_decorative_bubble_cluster_timer() - else: - _schedule_next_decorative_bubble_event( - BUBBLE_EVENT_DELAY_MIN, - BUBBLE_EVENT_DELAY_MAX - ) - - -func _spawn_decorative_bubble( - generation: int, - start_y_ratio: float = -1.0, - normalized_x_override: float = -1.0, - is_transition_bubble: bool = false, -) -> bool: - if ( - generation != _decorative_generation - or not _decorative_presentation_active - or not visible - or DECORATIVE_BUBBLE_TEXTURES.is_empty() - ): - return false - if is_transition_bubble: - if _transition_bubble_ids.size() >= MAX_TRANSITION_BUBBLES: - return false - elif _get_idle_decorative_bubble_count() >= MAX_DECORATIVE_BUBBLES: - return false - var layer_size: Vector2 = _decorative_bubble_layer.size - if layer_size.x <= 1.0 or layer_size.y <= 1.0: - return false - var texture: Texture2D = DECORATIVE_BUBBLE_TEXTURES[ - _decorative_rng.randi_range( - 0, - DECORATIVE_BUBBLE_TEXTURES.size() - 1 - ) - ] - var presentation_scale: float = _decorative_rng.randf_range( - BUBBLE_SCALE_MIN, - BUBBLE_SCALE_MAX - ) - var presentation_size: Vector2 = texture.get_size() * presentation_scale - var bubble := TextureRect.new() - bubble.name = "DecorativeBubble" - bubble.texture = texture - bubble.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - bubble.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED - bubble.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST - bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE - bubble.size = presentation_size - bubble.modulate.a = _decorative_rng.randf_range( - BUBBLE_OPACITY_MIN, - BUBBLE_OPACITY_MAX - ) - _decorative_bubble_layer.add_child(bubble) - _decorative_bubbles.append(bubble) - if is_transition_bubble: - _transition_bubble_ids[bubble.get_instance_id()] = true - - var normalized_x: float = normalized_x_override - if normalized_x < 0.0: - normalized_x = _decorative_rng.randf_range(0.08, 0.92) - var drift_direction: float = ( - 1.0 if _decorative_rng.randi_range(0, 1) == 1 else -1.0 - ) - var horizontal_drift: float = ( - _decorative_rng.randf_range( - BUBBLE_DRIFT_MIN, - BUBBLE_DRIFT_MAX - ) - * drift_direction - ) - var wobble_amplitude: float = _decorative_rng.randf_range( - BUBBLE_WOBBLE_MIN, - BUBBLE_WOBBLE_MAX - ) - var wobble_cycles: float = _decorative_rng.randf_range(0.8, 1.6) - var wobble_phase: float = _decorative_rng.randf_range(0.0, TAU) - var travel_duration: float = _decorative_rng.randf_range( - BUBBLE_TRAVEL_DURATION_MIN, - BUBBLE_TRAVEL_DURATION_MAX - ) - if start_y_ratio >= 0.0: - var full_start_y: float = ( - layer_size.y + presentation_size.y + BUBBLE_EDGE_MARGIN - ) - var end_y: float = -presentation_size.y - BUBBLE_EDGE_MARGIN - var burst_start_y: float = ( - start_y_ratio * layer_size.y - presentation_size.y * 0.5 - ) - var remaining_distance: float = maxf(burst_start_y - end_y, 1.0) - var full_distance: float = maxf(full_start_y - end_y, 1.0) - travel_duration *= remaining_distance / full_distance - _update_decorative_bubble( - 0.0, - bubble, - normalized_x, - horizontal_drift, - wobble_amplitude, - wobble_cycles, - wobble_phase, - start_y_ratio, - generation - ) - var bubble_tween: Tween = create_tween() - _decorative_bubble_tweens[bubble.get_instance_id()] = bubble_tween - bubble_tween.tween_method( - _update_decorative_bubble.bind( - bubble, - normalized_x, - horizontal_drift, - wobble_amplitude, - wobble_cycles, - wobble_phase, - start_y_ratio, - generation - ), - 0.0, - 1.0, - travel_duration - ) - bubble_tween.finished.connect( - _on_decorative_bubble_finished.bind(bubble, generation), - CONNECT_ONE_SHOT - ) - return true - - -func _update_decorative_bubble( - progress: float, - bubble: TextureRect, - normalized_x: float, - horizontal_drift: float, - wobble_amplitude: float, - wobble_cycles: float, - wobble_phase: float, - start_y_ratio: float, - generation: int, -) -> void: - if ( - generation != _decorative_generation - or not _decorative_presentation_active - or not is_instance_valid(bubble) - ): - return - var layer_size: Vector2 = _decorative_bubble_layer.size - var start_y: float = ( - layer_size.y + bubble.size.y + BUBBLE_EDGE_MARGIN - ) - if start_y_ratio >= 0.0: - start_y = start_y_ratio * layer_size.y - bubble.size.y * 0.5 - var end_y: float = -bubble.size.y - BUBBLE_EDGE_MARGIN - var base_x: float = normalized_x * layer_size.x - var wobble: float = sin( - wobble_phase + progress * TAU * wobble_cycles - ) * wobble_amplitude - bubble.position = _snap_world_preview(Vector2( - base_x - + horizontal_drift * progress - + wobble - - bubble.size.x * 0.5, - lerpf(start_y, end_y, progress) - )) - - -func _on_decorative_bubble_finished( - bubble: TextureRect, - generation: int, -) -> void: - if generation != _decorative_generation: - return - if is_instance_valid(bubble): - _decorative_bubble_tweens.erase(bubble.get_instance_id()) - _transition_bubble_ids.erase(bubble.get_instance_id()) - _decorative_bubbles.erase(bubble) - bubble.queue_free() - - -func _get_idle_decorative_bubble_count() -> int: - return _decorative_bubbles.size() - _transition_bubble_ids.size() - - -func _emit_navigation_bubble_flurry() -> void: - # Page changes now use a quick fade and scale without travel bubbles. - pass - - -func _exit_tree() -> void: - _stop_decorative_presentation() diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index 653d13d..22f00f2 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -1,29 +1,36 @@ -[gd_scene load_steps=25 format=3] +[gd_scene load_steps=28 format=3] [ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] [ext_resource type="PackedScene" path="res://ui/settings_panel.tscn" id="3_settings"] -[ext_resource type="Shader" path="res://ui/title_water_background.gdshader" id="4_water_shader"] -[ext_resource type="Texture2D" path="res://ui/assets/title/netfishing_logo.png" id="5_logo"] +[ext_resource type="Shader" path="res://ui/player_menu_pattern.gdshader" id="4_grass_shader"] +[ext_resource type="Texture2D" path="res://ui/assets/title/straywild_logo.png" id="5_logo"] [ext_resource type="Shader" path="res://ui/title_logo_underwater.gdshader" id="6_logo_shader"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_button.gd" id="7_bubble_button"] [ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="8_bubble_cluster"] [ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="9_bubble_profile"] [ext_resource type="PackedScene" path="res://ui/title_confirmation_bubble_page.tscn" id="10_confirmation_page"] [ext_resource type="PackedScene" path="res://ui/network/join_game_page.tscn" id="11_join_page"] -[ext_resource type="Texture2D" path="res://ui/icons/pictograms/online_dark.png" id="12_online_dark"] -[ext_resource type="Texture2D" path="res://ui/icons/pictograms/settings_dark.png" id="13_settings_dark"] -[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_dark.png" id="14_x_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/new_menu/online.png" id="12_online_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/new_menu/settings.png" id="13_settings_dark"] +[ext_resource type="Texture2D" path="res://ui/icons/new_menu/close.png" id="14_x_dark"] [ext_resource type="PackedScene" path="res://ui/title_credits_page.tscn" id="15_credits_page"] -[ext_resource type="Texture2D" path="res://ui/icons/main_menu/continue.png" id="16_continue"] +[ext_resource type="Texture2D" path="res://ui/icons/new_menu/play.png" id="16_continue"] [ext_resource type="Texture2D" path="res://ui/icons/main_menu/new_game.png" id="17_new_game"] -[ext_resource type="Texture2D" path="res://ui/icons/main_menu/credits.png" id="18_credits"] +[ext_resource type="Texture2D" path="res://ui/icons/new_menu/credits.png" id="18_credits"] [ext_resource type="Texture2D" path="res://ui/icons/main_menu/delete_save.png" id="19_delete_save"] [ext_resource type="PackedScene" path="res://ui/new_game_setup_page.tscn" id="20_new_game_setup"] [ext_resource type="PackedScene" path="res://ui/save_slots_page.tscn" id="21_save_slots"] +[ext_resource type="Texture2D" path="res://world/generation/chunks/assets/chunk_0000_grass_lite.png" id="22_grass_pattern"] +[ext_resource type="Resource" path="res://ui/components/bubble_menu/title_menu_profile.tres" id="23_title_menu_profile"] +[ext_resource type="Resource" path="res://ui/components/bubble_menu/title_secondary_menu_profile.tres" id="24_title_secondary_menu_profile"] -[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"] -shader = ExtResource("4_water_shader") +[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_grass"] +shader = ExtResource("4_grass_shader") +shader_parameter/pattern_texture = ExtResource("22_grass_pattern") +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_title_logo"] shader = ExtResource("6_logo_shader") @@ -39,6 +46,17 @@ corner_radius_top_right = 14 corner_radius_bottom_right = 14 corner_radius_bottom_left = 14 +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_continue_stats_drawer"] +content_margin_left = 10.0 +content_margin_top = 18.0 +content_margin_right = 10.0 +content_margin_bottom = 2.0 +bg_color = Color(0.031, 0.122, 0.169, 1) +corner_radius_top_left = 14 +corner_radius_top_right = 14 +corner_radius_bottom_right = 14 +corner_radius_bottom_left = 14 + [node name="TitleScreen" type="Control"] unique_name_in_owner = true z_index = 200 @@ -55,7 +73,7 @@ script = ExtResource("1_script") [node name="Background" type="ColorRect" parent="."] unique_name_in_owner = true visible = false -material = SubResource("ShaderMaterial_title_water") +material = SubResource("ShaderMaterial_title_grass") layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -64,42 +82,6 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="DecorativeFishLayer" type="Control" parent="."] -unique_name_in_owner = true -z_index = 1 -clip_contents = true -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 - -[node name="DecorativeFishTimer" type="Timer" parent="."] -unique_name_in_owner = true -one_shot = true - -[node name="DecorativeBubbleLayer" type="Control" parent="."] -unique_name_in_owner = true -z_index = 2 -clip_contents = true -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 - -[node name="DecorativeBubbleEventTimer" type="Timer" parent="."] -unique_name_in_owner = true -one_shot = true - -[node name="DecorativeBubbleClusterTimer" type="Timer" parent="."] -unique_name_in_owner = true -one_shot = true - [node name="ResponsiveTitleStage" type="Control" parent="."] unique_name_in_owner = true z_index = 10 @@ -122,13 +104,13 @@ mouse_filter = 1 unique_name_in_owner = true z_index = 12 layout_mode = 1 -anchor_top = 1.0 +anchor_top = 0.5 anchor_right = 1.0 -anchor_bottom = 1.0 -offset_top = -104.0 -offset_bottom = -48.0 +anchor_bottom = 0.5 +offset_top = 45.0 +offset_bottom = 101.0 grow_horizontal = 2 -grow_vertical = 0 +grow_vertical = 2 mouse_filter = 2 [node name="StartPromptLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/StartPromptCenter"] @@ -136,8 +118,6 @@ unique_name_in_owner = true layout_mode = 2 mouse_filter = 2 theme_override_colors/font_color = Color(1, 1, 1, 1) -theme_override_colors/font_outline_color = Color(0.02, 0.075, 0.11, 0.9) -theme_override_constants/outline_size = 3 theme_override_font_sizes/font_size = 39 text = "press any key to start" horizontal_alignment = 1 @@ -202,32 +182,38 @@ texture = ExtResource("5_logo") expand_mode = 1 stretch_mode = 5 -[node name="VersionRow" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent"] +[node name="VersionRow" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 18) -layout_mode = 2 +z_index = 12 +custom_minimum_size = Vector2(280, 28) +layout_mode = 1 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -140.0 +offset_top = -38.0 +offset_right = 140.0 +offset_bottom = -10.0 +grow_horizontal = 2 +grow_vertical = 0 mouse_filter = 2 -[node name="VersionAnchor" type="CenterContainer" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent/VersionRow"] +[node name="VersionAnchor" type="CenterContainer" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/VersionRow"] layout_mode = 1 -anchor_left = 0.7 -anchor_top = 0.5 -anchor_right = 0.7 -anchor_bottom = 0.5 -offset_left = -140.0 -offset_top = -17.0 -offset_right = 140.0 -offset_bottom = 11.0 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="PlaytestLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/BrandingSlot/BrandingMotionRoot/BrandingContent/VersionRow/VersionAnchor"] +[node name="PlaytestLabel" type="Label" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/VersionRow/VersionAnchor"] unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) theme_override_font_sizes/font_size = 22 -text = "v0.18.0-alpha" +text = "v0.20.2-alpha" horizontal_alignment = 1 [node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] @@ -242,47 +228,75 @@ layout_mode = 2 [node name="BubbleLayoutSlot" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter"] unique_name_in_owner = true -custom_minimum_size = Vector2(440, 318) +custom_minimum_size = Vector2(500, 360) layout_mode = 2 [node name="BubbleMotionRoot" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot"] unique_name_in_owner = true layout_mode = 0 -offset_right = 440.0 -offset_bottom = 318.0 +offset_right = 500.0 +offset_bottom = 360.0 mouse_filter = 2 [node name="BubbleField" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot"] unique_name_in_owner = true -custom_minimum_size = Vector2(440, 318) +custom_minimum_size = Vector2(500, 360) layout_mode = 0 -offset_right = 440.0 -offset_bottom = 318.0 +offset_right = 500.0 +offset_top = -10.0 +offset_bottom = 350.0 script = ExtResource("8_bubble_cluster") -profile = ExtResource("9_bubble_profile") +profile = ExtResource("23_title_menu_profile") +desktop_reference_size = Vector2(500, 360) +collision_separation_enabled = false [node name="PlayButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true -offset_left = 106.0 -offset_top = 71.0 -offset_right = 290.0 -offset_bottom = 249.0 +offset_left = 36.0 +offset_top = 30.0 +offset_right = 366.0 +offset_bottom = 360.0 texture_filter = 1 icon = ExtResource("16_continue") -tooltip_text = "play" -accessibility_name = "play" +accessibility_name = "play straywild" script = ExtResource("7_bubble_button") -profile = ExtResource("9_bubble_profile") -neutral_size = Vector2(184, 178) -desktop_anchor = Vector2(198, 160) -compact_anchor = Vector2(198, 164) -minimum_font_size = 19 -maximum_font_size = 30 -horizontal_amplitude = 1.8 -vertical_amplitude = 4.6 -motion_period = 5.8 -deformation_amplitude = 0.01 -deformation_period = 6.7 +profile = ExtResource("23_title_menu_profile") +neutral_size = Vector2(330, 330) +desktop_anchor = Vector2(201, 195) +compact_anchor = Vector2(201, 195) +minimum_font_size = 28 +maximum_font_size = 36 +icon_fill_ratio_override = 0.9 +horizontal_amplitude = 0.0 +vertical_amplitude = 0.0 +deformation_amplitude = 0.0 +hover_wiggle_amplitude = Vector2(7, 6) +hover_wiggle_period = 3.8 +hover_drawer_text = "play straywild" +hover_drawer_mode = 1 +elliptical_hit_area = false + +[node name="ContinueStatsDrawer" type="RichTextLabel" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField/PlayButton"] +unique_name_in_owner = true +show_behind_parent = true +z_index = -1 +visible = false +layout_mode = 0 +offset_left = 22.0 +offset_top = 314.0 +offset_right = 308.0 +offset_bottom = 360.0 +pivot_offset = Vector2(143, 0) +scale = Vector2(1, 0.001) +mouse_filter = 2 +theme_override_font_sizes/normal_font_size = 15 +text = "" +bbcode_enabled = true +scroll_active = false +autowrap_mode = 2 +horizontal_alignment = 1 +vertical_alignment = 1 +theme_override_styles/normal = SubResource("StyleBoxFlat_continue_stats_drawer") [node name="NewGameButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true @@ -313,75 +327,75 @@ deformation_period = 5.9 [node name="SettingsButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true -offset_left = 254.0 -offset_top = 12.0 -offset_right = 374.0 -offset_bottom = 128.0 +offset_left = 384.0 +offset_top = 116.0 +offset_right = 456.0 +offset_bottom = 188.0 texture_filter = 1 icon = ExtResource("13_settings_dark") -tooltip_text = "settings" accessibility_name = "settings" script = ExtResource("7_bubble_button") -profile = ExtResource("9_bubble_profile") -desktop_anchor = Vector2(314, 70) -compact_anchor = Vector2(333, 75) -compact_minimum_size = Vector2(94, 90) +profile = ExtResource("24_title_secondary_menu_profile") +neutral_size = Vector2(72, 72) +desktop_anchor = Vector2(420, 152) +compact_anchor = Vector2(420, 152) minimum_font_size = 15 -maximum_font_size = 22 -vertical_amplitude = 3.7 -motion_period = 4.9 -motion_phase = 2.5 -deformation_period = 6.3 +maximum_font_size = 20 +horizontal_amplitude = 0.0 +vertical_amplitude = 0.0 +deformation_amplitude = 0.0 +hover_wiggle_amplitude = Vector2(2.4, 1.2) +hover_wiggle_period = 1.9 +hover_drawer_text = "settings" +elliptical_hit_area = false [node name="CreditsButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true -offset_left = 54.0 -offset_top = 209.0 -offset_right = 138.0 -offset_bottom = 291.0 +offset_left = 384.0 +offset_top = 202.0 +offset_right = 456.0 +offset_bottom = 274.0 texture_filter = 1 icon = ExtResource("18_credits") -tooltip_text = "credits" accessibility_name = "credits" script = ExtResource("7_bubble_button") -profile = ExtResource("9_bubble_profile") -neutral_size = Vector2(84, 82) -desktop_anchor = Vector2(96, 250) -compact_anchor = Vector2(82, 258) -compact_minimum_size = Vector2(62, 62) -minimum_font_size = 12 -maximum_font_size = 17 -horizontal_amplitude = 1.4 -vertical_amplitude = 3.0 -motion_period = 5.0 -motion_phase = 1.8 -deformation_amplitude = 0.015 -deformation_period = 5.6 +profile = ExtResource("24_title_secondary_menu_profile") +neutral_size = Vector2(72, 72) +desktop_anchor = Vector2(420, 238) +compact_anchor = Vector2(420, 238) +minimum_font_size = 15 +maximum_font_size = 20 +horizontal_amplitude = 0.0 +vertical_amplitude = 0.0 +deformation_amplitude = 0.0 +hover_wiggle_amplitude = Vector2(2.4, 1.2) +hover_wiggle_period = 1.9 +hover_drawer_text = "credits" +elliptical_hit_area = false [node name="JoinGameButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true -offset_left = 21.0 -offset_top = 11.0 -offset_right = 143.0 -offset_bottom = 129.0 +offset_left = 384.0 +offset_top = 30.0 +offset_right = 456.0 +offset_bottom = 102.0 texture_filter = 1 icon = ExtResource("12_online_dark") -tooltip_text = "join game" -accessibility_name = "join game" +accessibility_name = "online" script = ExtResource("7_bubble_button") -profile = ExtResource("9_bubble_profile") -neutral_size = Vector2(122, 118) -desktop_anchor = Vector2(82, 70) -compact_anchor = Vector2(63, 75) -compact_minimum_size = Vector2(82, 80) +profile = ExtResource("24_title_secondary_menu_profile") +neutral_size = Vector2(72, 72) +desktop_anchor = Vector2(420, 66) +compact_anchor = Vector2(420, 66) minimum_font_size = 15 -maximum_font_size = 22 -horizontal_amplitude = 1.7 -vertical_amplitude = 3.8 -motion_period = 5.2 -motion_phase = 3.1 -deformation_amplitude = 0.015 -deformation_period = 5.7 +maximum_font_size = 20 +horizontal_amplitude = 0.0 +vertical_amplitude = 0.0 +deformation_amplitude = 0.0 +hover_wiggle_amplitude = Vector2(2.4, 1.2) +hover_wiggle_period = 1.9 +hover_drawer_text = "online" +elliptical_hit_area = false [node name="DeleteSaveButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true @@ -411,35 +425,41 @@ deformation_period = 5.4 [node name="QuitButton" type="Button" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent/ButtonCenter/BubbleLayoutSlot/BubbleMotionRoot/BubbleField"] unique_name_in_owner = true -offset_left = 255.0 -offset_top = 204.0 -offset_right = 349.0 -offset_bottom = 296.0 +offset_left = 384.0 +offset_top = 288.0 +offset_right = 456.0 +offset_bottom = 360.0 texture_filter = 1 icon = ExtResource("14_x_dark") -tooltip_text = "quit" -accessibility_name = "quit" +accessibility_name = "close" script = ExtResource("7_bubble_button") -profile = ExtResource("9_bubble_profile") -neutral_size = Vector2(94, 92) -desktop_anchor = Vector2(302, 250) -compact_anchor = Vector2(317, 254) -compact_minimum_size = Vector2(68, 68) +profile = ExtResource("24_title_secondary_menu_profile") +neutral_size = Vector2(72, 72) +desktop_anchor = Vector2(420, 324) +compact_anchor = Vector2(420, 324) minimum_font_size = 14 -maximum_font_size = 18 -horizontal_amplitude = 1.4 -vertical_amplitude = 3.2 -motion_period = 4.2 -motion_phase = 4.8 -deformation_amplitude = 0.02 -deformation_period = 4.8 +maximum_font_size = 20 +horizontal_amplitude = 0.0 +vertical_amplitude = 0.0 +deformation_amplitude = 0.0 +hover_wiggle_amplitude = Vector2(2.4, 1.2) +hover_wiggle_period = 1.9 +hover_drawer_text = "close" +elliptical_hit_area = false -[node name="FeedbackLabel" type="RichTextLabel" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] +[node name="FeedbackLabel" type="RichTextLabel" parent="ResponsiveTitleStage/TitlePresentationScaleRoot"] unique_name_in_owner = true +z_index = 12 visible = false -custom_minimum_size = Vector2(430, 44) -layout_mode = 2 -size_flags_horizontal = 4 +custom_minimum_size = Vector2(500, 44) +layout_mode = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -250.0 +offset_top = 224.0 +offset_right = 250.0 +offset_bottom = 268.0 +grow_horizontal = 2 text = "" bbcode_enabled = true scroll_active = false diff --git a/ui/title_water_background.gdshader b/ui/title_water_background.gdshader deleted file mode 100644 index ca12bc6..0000000 --- a/ui/title_water_background.gdshader +++ /dev/null @@ -1,97 +0,0 @@ -shader_type canvas_item; - -uniform vec4 surface_color : source_color = vec4(0.10, 0.45, 0.62, 1.0); -uniform vec4 middle_color : source_color = vec4(0.055, 0.285, 0.40, 1.0); -uniform vec4 depth_color : source_color = vec4(0.02, 0.16, 0.27, 1.0); -uniform vec2 virtual_pixel_density = vec2(320.0, 180.0); -uniform float color_step_count : hint_range(2.0, 16.0, 1.0) = 14.0; -uniform float continuous_depth_blend : hint_range(0.0, 0.25, 0.01) = 0.12; -uniform float wave_speed : hint_range(0.0, 0.5, 0.01) = 0.10; -uniform float wave_scale : hint_range(0.5, 8.0, 0.1) = 2.4; -uniform float wave_strength : hint_range(0.0, 0.05, 0.001) = 0.012; -uniform float caustic_strength : hint_range(0.0, 0.08, 0.001) = 0.018; -uniform bool animation_enabled = true; - -void fragment() { - vec2 grid_size = max(virtual_pixel_density, vec2(1.0)); - vec2 pixel_uv = (floor(UV * grid_size) + vec2(0.5)) / grid_size; - float continuous_depth = pixel_uv.y; - float caustic_level = 0.0; - if (animation_enabled) { - float motion = TIME * wave_speed; - float broad_wave = sin( - pixel_uv.x * wave_scale + motion - ); - float secondary_wave = sin( - pixel_uv.x * 1.1 + pixel_uv.y * 1.6 - motion * 0.65 - ); - float movement_falloff = 1.0 - pixel_uv.y * 0.75; - float depth_displacement = ( - (broad_wave * 0.65 + secondary_wave * 0.35) - * wave_strength - * movement_falloff - ); - continuous_depth = clamp( - pixel_uv.y - + depth_displacement, - 0.0, - 1.0 - ); - float caustic_wave = clamp( - ( - sin( - pixel_uv.x * 4.0 - + pixel_uv.y * 1.2 - + motion * 0.7 - ) - * sin( - pixel_uv.x * 1.7 - - pixel_uv.y * 2.2 - - motion * 0.55 - ) - - 0.25 - ) / 0.75, - 0.0, - 1.0 - ); - caustic_level = floor(caustic_wave * 3.0) / 3.0; - } - float depth_steps = max(2.0, floor(color_step_count)); - float band_index = floor( - continuous_depth * (depth_steps - 1.0) + 0.5 - ); - float band_depth = band_index / (depth_steps - 1.0); - float display_depth = mix( - band_depth, - continuous_depth, - clamp(continuous_depth_blend, 0.0, 0.25) - ); - - vec3 water_color; - if (display_depth < 0.5) { - water_color = mix( - surface_color.rgb, - middle_color.rgb, - display_depth * 2.0 - ); - } else { - water_color = mix( - middle_color.rgb, - depth_color.rgb, - (display_depth - 0.5) * 2.0 - ); - } - - float upper_water_mask = 1.0 - smoothstep( - 0.18, - 0.34, - pixel_uv.y - ); - water_color += ( - vec3(0.05, 0.12, 0.14) - * caustic_level - * upper_water_mask - * caustic_strength - ); - COLOR = vec4(clamp(water_color, vec3(0.0), vec3(1.0)), 1.0); -} diff --git a/ui/title_water_background.gdshader.uid b/ui/title_water_background.gdshader.uid deleted file mode 100644 index fda761b..0000000 --- a/ui/title_water_background.gdshader.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c8s1g7tn771du diff --git a/world/decor_shop_interaction.gd b/world/decor_shop_interaction.gd new file mode 100644 index 0000000..a325134 --- /dev/null +++ b/world/decor_shop_interaction.gd @@ -0,0 +1,10 @@ +class_name DecorShopInteraction +extends FishingShopInteraction + +const DECOR_DIALOGUE_TEXT: String = ( + "You gonna decorate that hot box or what? I've got some new decor in, take a look." +) + + +func get_dialogue_text(_home_tier: int = -1) -> String: + return DECOR_DIALOGUE_TEXT diff --git a/world/decor_shop_interaction.gd.uid b/world/decor_shop_interaction.gd.uid new file mode 100644 index 0000000..955738c --- /dev/null +++ b/world/decor_shop_interaction.gd.uid @@ -0,0 +1 @@ +uid://u70jgwedk7ym diff --git a/world/digging/diggable_area_3d.gd b/world/digging/diggable_area_3d.gd index 47270b1..4290b44 100644 --- a/world/digging/diggable_area_3d.gd +++ b/world/digging/diggable_area_3d.gd @@ -2,6 +2,10 @@ class_name DiggableArea3D extends Node3D +const SURFACE_HEIGHT_TOLERANCE := 0.015 +const SURFACE_BOUNDS_TOLERANCE := 0.001 +const MINIMUM_VALIDATION_UP_DOT := 0.01 + @export var area_id: StringName @export_node_path("Node3D") var terrain_source: NodePath @export var surface_materials: Array[StringName] = [] @@ -10,9 +14,14 @@ extends Node3D @export_range(-100.0, 100.0, 0.01) var maximum_global_y: float = 100.0 @export_range(0.0, 1.0, 0.01) var minimum_up_dot: float = 0.6 +var _surface_validation_groups: Array[Dictionary] = [] +var _surface_validation_ready := false + func get_surface_triangles() -> Array[PackedVector3Array]: var triangles: Array[PackedVector3Array] = [] + _surface_validation_groups.clear() + _surface_validation_ready = false if area_id.is_empty() or surface_materials.is_empty(): return triangles var terrain_root: Node = get_node_or_null(terrain_source) @@ -22,25 +31,90 @@ func get_surface_triangles() -> Array[PackedVector3Array]: var mesh: Mesh = mesh_instance.mesh if mesh == null: continue + var validation_records: Array[Dictionary] = [] + var validation_bounds := Rect2() + var has_validation_bounds := false for surface_index: int in mesh.get_surface_count(): var material: Material = mesh_instance.get_active_material(surface_index) - if ( - material == null - or not surface_materials.has(StringName(material.resource_name)) - ): - continue + var allowed_surface := ( + material != null + and surface_materials.has(StringName(material.resource_name)) + ) _append_surface_triangles( triangles, + validation_records, mesh_instance, mesh.surface_get_arrays(surface_index), + allowed_surface, ) + for record: Dictionary in validation_records: + var record_bounds: Rect2 = record.get("bounds", Rect2()) + if not has_validation_bounds: + validation_bounds = record_bounds + has_validation_bounds = true + else: + validation_bounds = validation_bounds.merge(record_bounds) + if not validation_records.is_empty(): + _surface_validation_groups.append({ + "bounds": validation_bounds, + "records": validation_records, + }) + _surface_validation_ready = true return triangles +func invalidate_surface_cache() -> void: + _surface_validation_groups.clear() + _surface_validation_ready = false + + +func is_surface_point_valid(point: Vector3) -> bool: + if not point.is_finite(): + return false + if not _surface_validation_ready: + get_surface_triangles() + var sample := Vector2(point.x, point.z) + var highest_y := -INF + var highest_is_allowed := false + var found_surface := false + for group: Dictionary in _surface_validation_groups: + var group_bounds: Rect2 = group.get("bounds", Rect2()) + if not group_bounds.grow(SURFACE_BOUNDS_TOLERANCE).has_point(sample): + continue + var records: Array = group.get("records", []) + for record: Dictionary in records: + var record_bounds: Rect2 = record.get("bounds", Rect2()) + if not record_bounds.grow(SURFACE_BOUNDS_TOLERANCE).has_point(sample): + continue + var triangle := record.get( + "triangle", PackedVector3Array() + ) as PackedVector3Array + var surface_y := _surface_height_at_point(triangle, sample) + if not is_finite(surface_y): + continue + var allowed := bool(record.get("allowed", false)) + if surface_y > highest_y + SURFACE_HEIGHT_TOLERANCE: + highest_y = surface_y + highest_is_allowed = allowed + found_surface = true + elif absf(surface_y - highest_y) <= SURFACE_HEIGHT_TOLERANCE: + # Material seams may expose coplanar grass and sand triangles. + # Treat any tied disallowed surface as authoritative so a dig + # hotspot can never appear in visually grassy terrain. + highest_is_allowed = highest_is_allowed and allowed + return ( + found_surface + and highest_is_allowed + and absf(highest_y - point.y) <= SURFACE_HEIGHT_TOLERANCE + ) + + func _append_surface_triangles( result: Array[PackedVector3Array], + validation_records: Array[Dictionary], mesh_instance: MeshInstance3D, arrays: Array, + allowed_surface: bool, ) -> void: if arrays.size() <= Mesh.ARRAY_INDEX: return @@ -52,26 +126,49 @@ func _append_surface_triangles( for vertex_index: int in range(0, vertices.size() - 2, 3): _append_triangle( result, + validation_records, mesh_instance.to_global(vertices[vertex_index]), mesh_instance.to_global(vertices[vertex_index + 1]), mesh_instance.to_global(vertices[vertex_index + 2]), + allowed_surface, ) return for index_offset: int in range(0, indices.size() - 2, 3): _append_triangle( result, + validation_records, mesh_instance.to_global(vertices[indices[index_offset]]), mesh_instance.to_global(vertices[indices[index_offset + 1]]), mesh_instance.to_global(vertices[indices[index_offset + 2]]), + allowed_surface, ) func _append_triangle( result: Array[PackedVector3Array], + validation_records: Array[Dictionary], a: Vector3, b: Vector3, c: Vector3, + allowed_surface: bool, ) -> void: + var cross := (b - a).cross(c - a) + if cross.length_squared() <= 0.0000001: + return + var up_dot := absf(cross.normalized().dot(Vector3.UP)) + var triangle := PackedVector3Array([a, b, c]) + var projected_bounds := _projected_bounds(triangle) + if ( + up_dot >= MINIMUM_VALIDATION_UP_DOT + and generation_bounds.intersects(projected_bounds, true) + ): + validation_records.append({ + "allowed": allowed_surface, + "bounds": projected_bounds, + "triangle": triangle, + }) + if not allowed_surface: + return if ( a.y < minimum_global_y or b.y < minimum_global_y @@ -84,12 +181,56 @@ func _append_triangle( var center := (a + b + c) / 3.0 if not generation_bounds.has_point(Vector2(center.x, center.z)): return - var cross := (b - a).cross(c - a) - if cross.length_squared() <= 0.0000001: + if up_dot < minimum_up_dot: return - if absf(cross.normalized().dot(Vector3.UP)) < minimum_up_dot: - return - result.append(PackedVector3Array([a, b, c])) + result.append(triangle) + + +static func _projected_bounds(triangle: PackedVector3Array) -> Rect2: + var minimum := Vector2(triangle[0].x, triangle[0].z) + var maximum := minimum + for index: int in range(1, triangle.size()): + var point := Vector2(triangle[index].x, triangle[index].z) + minimum = minimum.min(point) + maximum = maximum.max(point) + return Rect2(minimum, maximum - minimum) + + +static func _surface_height_at_point( + triangle: PackedVector3Array, + point: Vector2, +) -> float: + if triangle.size() != 3: + return INF + 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 := ( + (b.y - c.y) * (a.x - c.x) + + (c.x - b.x) * (a.y - c.y) + ) + if absf(denominator) <= 0.0000001: + return INF + var weight_a := ( + (b.y - c.y) * (point.x - c.x) + + (c.x - b.x) * (point.y - c.y) + ) / denominator + var weight_b := ( + (c.y - a.y) * (point.x - c.x) + + (a.x - c.x) * (point.y - c.y) + ) / denominator + var weight_c := 1.0 - weight_a - weight_b + if ( + weight_a < -SURFACE_BOUNDS_TOLERANCE + or weight_b < -SURFACE_BOUNDS_TOLERANCE + or weight_c < -SURFACE_BOUNDS_TOLERANCE + ): + return INF + return ( + triangle[0].y * weight_a + + triangle[1].y * weight_b + + triangle[2].y * weight_c + ) func _terrain_mesh_instances(terrain_root: Node) -> Array[MeshInstance3D]: diff --git a/world/environment/netfishing_environment.tres b/world/environment/straywild_environment.tres similarity index 87% rename from world/environment/netfishing_environment.tres rename to world/environment/straywild_environment.tres index 4f625e4..f4c5b75 100644 --- a/world/environment/netfishing_environment.tres +++ b/world/environment/straywild_environment.tres @@ -1,6 +1,6 @@ [gd_resource type="Environment" load_steps=2 format=3] -[ext_resource type="Sky" path="res://world/environment/netfishing_sky.tres" id="1_sky"] +[ext_resource type="Sky" path="res://world/environment/straywild_sky.tres" id="1_sky"] [resource] background_mode = 2 diff --git a/world/environment/netfishing_sky.gdshader b/world/environment/straywild_sky.gdshader similarity index 98% rename from world/environment/netfishing_sky.gdshader rename to world/environment/straywild_sky.gdshader index 22af7ad..83defee 100644 --- a/world/environment/netfishing_sky.gdshader +++ b/world/environment/straywild_sky.gdshader @@ -1,6 +1,6 @@ shader_type sky; -#include "res://world/environment/netfishing_sky_clouds.gdshaderinc" +#include "res://world/environment/straywild_sky_clouds.gdshaderinc" uniform vec4 sky_top_color : source_color = vec4(0.204, 0.498, 0.643, 1.0); uniform vec4 sky_horizon_color : source_color = vec4(0.663, 0.843, 0.847, 1.0); diff --git a/world/environment/netfishing_sky.gdshader.uid b/world/environment/straywild_sky.gdshader.uid similarity index 100% rename from world/environment/netfishing_sky.gdshader.uid rename to world/environment/straywild_sky.gdshader.uid diff --git a/world/environment/netfishing_sky.tres b/world/environment/straywild_sky.tres similarity index 89% rename from world/environment/netfishing_sky.tres rename to world/environment/straywild_sky.tres index 98fb4e0..553b687 100644 --- a/world/environment/netfishing_sky.tres +++ b/world/environment/straywild_sky.tres @@ -1,8 +1,8 @@ [gd_resource type="Sky" format=3 uid="uid://dersyay71dyat"] -[ext_resource type="Shader" uid="uid://chbcdlhui3ro4" path="res://world/environment/netfishing_sky.gdshader" id="1_sky_shader"] +[ext_resource type="Shader" uid="uid://chbcdlhui3ro4" path="res://world/environment/straywild_sky.gdshader" id="1_sky_shader"] -[sub_resource type="ShaderMaterial" id="ShaderMaterial_netfishing"] +[sub_resource type="ShaderMaterial" id="ShaderMaterial_straywild"] shader = ExtResource("1_sky_shader") shader_parameter/cloud_coverage = 0.0 shader_parameter/cloud_opacity = 0.0 @@ -36,4 +36,4 @@ shader_parameter/star_visibility = 0.0 shader_parameter/star_strength = 0.72 [resource] -sky_material = SubResource("ShaderMaterial_netfishing") +sky_material = SubResource("ShaderMaterial_straywild") diff --git a/world/environment/netfishing_sky_clouds.gdshaderinc b/world/environment/straywild_sky_clouds.gdshaderinc similarity index 100% rename from world/environment/netfishing_sky_clouds.gdshaderinc rename to world/environment/straywild_sky_clouds.gdshaderinc diff --git a/world/environment/netfishing_sky_clouds.gdshaderinc.uid b/world/environment/straywild_sky_clouds.gdshaderinc.uid similarity index 100% rename from world/environment/netfishing_sky_clouds.gdshaderinc.uid rename to world/environment/straywild_sky_clouds.gdshaderinc.uid diff --git a/world/fishing_shop_interaction.gd b/world/fishing_shop_interaction.gd index d912eca..9628ee6 100644 --- a/world/fishing_shop_interaction.gd +++ b/world/fishing_shop_interaction.gd @@ -3,6 +3,10 @@ extends Area3D signal local_player_range_changed(in_range: bool) +const DIALOGUE_TEXT: String = ( + "Listen, you may suck at fishing but I've got some things that might help." +) + @export_node_path("Node3D") var look_character_path: NodePath = ^"../Shopkeeper" var _local_player: Player @@ -37,6 +41,10 @@ func is_avatar_in_range(avatar: Player) -> bool: ) +func get_dialogue_text(_home_tier: int = -1) -> String: + return DIALOGUE_TEXT + + func get_prompt_anchor_position() -> Vector3: if _look_character != null: return _look_character.get_head_anchor_position() diff --git a/world/generation/chunks/assets/chunk_0003.glb b/world/generation/chunks/assets/chunk_0003.glb index 815aa8f..bb67914 100644 Binary files a/world/generation/chunks/assets/chunk_0003.glb and b/world/generation/chunks/assets/chunk_0003.glb differ diff --git a/world/generation/chunks/assets/chunk_0004.glb b/world/generation/chunks/assets/chunk_0004.glb index 4756e0f..fa093c0 100644 Binary files a/world/generation/chunks/assets/chunk_0004.glb and b/world/generation/chunks/assets/chunk_0004.glb differ diff --git a/world/generation/chunks/assets/chunk_0016.glb b/world/generation/chunks/assets/chunk_0016.glb index 7953824..dbf51a3 100644 Binary files a/world/generation/chunks/assets/chunk_0016.glb and b/world/generation/chunks/assets/chunk_0016.glb differ diff --git a/world/generation/chunks/assets/chunk_0018.glb b/world/generation/chunks/assets/chunk_0018.glb index cc8a4cb..16d6bf9 100644 Binary files a/world/generation/chunks/assets/chunk_0018.glb and b/world/generation/chunks/assets/chunk_0018.glb differ diff --git a/world/generation/chunks/assets/chunk_0020.glb b/world/generation/chunks/assets/chunk_0020.glb index 831fa49..3a1894f 100644 Binary files a/world/generation/chunks/assets/chunk_0020.glb and b/world/generation/chunks/assets/chunk_0020.glb differ diff --git a/world/generation/chunks/assets/chunk_0021.glb b/world/generation/chunks/assets/chunk_0021.glb index c7042ec..09562af 100644 Binary files a/world/generation/chunks/assets/chunk_0021.glb and b/world/generation/chunks/assets/chunk_0021.glb differ diff --git a/world/generation/chunks/assets/chunk_0022.glb b/world/generation/chunks/assets/chunk_0022.glb index 84ff40e..ea430c0 100644 Binary files a/world/generation/chunks/assets/chunk_0022.glb and b/world/generation/chunks/assets/chunk_0022.glb differ diff --git a/world/generation/chunks/assets/chunk_0024.glb b/world/generation/chunks/assets/chunk_0024.glb index 66a73dd..3266df4 100644 Binary files a/world/generation/chunks/assets/chunk_0024.glb and b/world/generation/chunks/assets/chunk_0024.glb differ diff --git a/world/generation/chunks/assets/chunk_0025.glb b/world/generation/chunks/assets/chunk_0025.glb index a0a0447..0ff38e9 100644 Binary files a/world/generation/chunks/assets/chunk_0025.glb and b/world/generation/chunks/assets/chunk_0025.glb differ diff --git a/world/generation/chunks/assets/chunk_0029.glb b/world/generation/chunks/assets/chunk_0029.glb index 78d4301..d3a6208 100644 Binary files a/world/generation/chunks/assets/chunk_0029.glb and b/world/generation/chunks/assets/chunk_0029.glb differ diff --git a/world/generation/chunks/assets/chunk_0030.glb b/world/generation/chunks/assets/chunk_0030.glb index c02801b..9cd6c4d 100644 Binary files a/world/generation/chunks/assets/chunk_0030.glb and b/world/generation/chunks/assets/chunk_0030.glb differ diff --git a/world/generation/chunks/assets/chunk_0031.glb b/world/generation/chunks/assets/chunk_0031.glb index 4815347..1293a19 100644 Binary files a/world/generation/chunks/assets/chunk_0031.glb and b/world/generation/chunks/assets/chunk_0031.glb differ diff --git a/world/generation/chunks/assets/chunk_0032.glb b/world/generation/chunks/assets/chunk_0032.glb index 7712aac..28fec93 100644 Binary files a/world/generation/chunks/assets/chunk_0032.glb and b/world/generation/chunks/assets/chunk_0032.glb differ diff --git a/world/generation/chunks/assets/chunk_0033.glb b/world/generation/chunks/assets/chunk_0033.glb index 648a089..03413af 100644 Binary files a/world/generation/chunks/assets/chunk_0033.glb and b/world/generation/chunks/assets/chunk_0033.glb differ diff --git a/world/generation/chunks/assets/chunk_0034.glb b/world/generation/chunks/assets/chunk_0034.glb index eaea7d1..7fc32e4 100644 Binary files a/world/generation/chunks/assets/chunk_0034.glb and b/world/generation/chunks/assets/chunk_0034.glb differ diff --git a/world/generation/chunks/assets/chunk_0035.glb b/world/generation/chunks/assets/chunk_0035.glb index 2eae4e7..a05d016 100644 Binary files a/world/generation/chunks/assets/chunk_0035.glb and b/world/generation/chunks/assets/chunk_0035.glb differ diff --git a/world/generation/chunks/assets/chunk_0036.glb b/world/generation/chunks/assets/chunk_0036.glb index e063e0c..52561b2 100644 Binary files a/world/generation/chunks/assets/chunk_0036.glb and b/world/generation/chunks/assets/chunk_0036.glb differ diff --git a/world/generation/chunks/assets/chunk_0037.glb b/world/generation/chunks/assets/chunk_0037.glb index ea79505..2d9a236 100644 Binary files a/world/generation/chunks/assets/chunk_0037.glb and b/world/generation/chunks/assets/chunk_0037.glb differ diff --git a/world/generation/chunks/assets/chunk_0038.glb b/world/generation/chunks/assets/chunk_0038.glb index 5fa83e3..2807d4e 100644 Binary files a/world/generation/chunks/assets/chunk_0038.glb and b/world/generation/chunks/assets/chunk_0038.glb differ diff --git a/world/generation/chunks/definitions/chunk_9996.tres b/world/generation/chunks/definitions/chunk_9996.tres index 5613d89..30f08d3 100644 --- a/world/generation/chunks/definitions/chunk_9996.tres +++ b/world/generation/chunks/definitions/chunk_9996.tres @@ -1,7 +1,7 @@ [gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3] [ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] -[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0015.glb" id="2_scene"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0010.glb" id="2_scene"] [ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0008.glb" id="3_base_coast"] [resource] @@ -9,7 +9,7 @@ script = ExtResource("1_definition") stable_id = &"chunk_9996" label = "second-tier beach sea transition left" packed_scene = ExtResource("2_scene") -primary_mesh_name = &"chunk_0015" +primary_mesh_name = &"chunk_0010" base_layer_scene = ExtResource("3_base_coast") base_layer_mesh_name = &"chunk_0008" base_layer_quarter_turns = 1 diff --git a/world/generation/chunks/definitions/chunk_9997.tres b/world/generation/chunks/definitions/chunk_9997.tres index 429db52..bf6b4e5 100644 --- a/world/generation/chunks/definitions/chunk_9997.tres +++ b/world/generation/chunks/definitions/chunk_9997.tres @@ -1,7 +1,7 @@ [gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3] [ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] -[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0015.glb" id="2_scene"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0010.glb" id="2_scene"] [ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0008.glb" id="3_base_coast"] [resource] @@ -9,7 +9,7 @@ script = ExtResource("1_definition") stable_id = &"chunk_9997" label = "second-tier beach sea transition right" packed_scene = ExtResource("2_scene") -primary_mesh_name = &"chunk_0015" +primary_mesh_name = &"chunk_0010" base_layer_scene = ExtResource("3_base_coast") base_layer_mesh_name = &"chunk_0008" base_layer_quarter_turns = 3 diff --git a/world/generation/chunks/definitions/chunk_9998.tres b/world/generation/chunks/definitions/chunk_9998.tres index a2e1dbf..fb12e20 100644 --- a/world/generation/chunks/definitions/chunk_9998.tres +++ b/world/generation/chunks/definitions/chunk_9998.tres @@ -1,7 +1,7 @@ [gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3] [ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] -[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0015.glb" id="2_scene"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0010.glb" id="2_scene"] [ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0007.glb" id="3_base_coast"] [resource] @@ -9,7 +9,7 @@ script = ExtResource("1_definition") stable_id = &"chunk_9998" label = "second-tier grass sea transition left" packed_scene = ExtResource("2_scene") -primary_mesh_name = &"chunk_0015" +primary_mesh_name = &"chunk_0010" base_layer_scene = ExtResource("3_base_coast") base_layer_mesh_name = &"chunk_0007" base_layer_quarter_turns = 1 diff --git a/world/generation/chunks/definitions/chunk_9999.tres b/world/generation/chunks/definitions/chunk_9999.tres index 8b4f99e..130d65d 100644 --- a/world/generation/chunks/definitions/chunk_9999.tres +++ b/world/generation/chunks/definitions/chunk_9999.tres @@ -1,7 +1,7 @@ [gd_resource type="Resource" script_class="TerrainChunkDefinition" load_steps=4 format=3] [ext_resource type="Script" path="res://world/generation/terrain_chunk_definition.gd" id="1_definition"] -[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0015.glb" id="2_scene"] +[ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0010.glb" id="2_scene"] [ext_resource type="PackedScene" path="res://world/generation/chunks/assets/chunk_0007.glb" id="3_base_coast"] [resource] @@ -9,7 +9,7 @@ script = ExtResource("1_definition") stable_id = &"chunk_9999" label = "second-tier grass sea transition right" packed_scene = ExtResource("2_scene") -primary_mesh_name = &"chunk_0015" +primary_mesh_name = &"chunk_0010" base_layer_scene = ExtResource("3_base_coast") base_layer_mesh_name = &"chunk_0007" base_layer_quarter_turns = 3 diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index 78188e0..61484fd 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -9,6 +9,12 @@ const FishingShopInteractionType = preload( const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const PrecipitationOcclusionType = preload( "res://world/environment/precipitation_occlusion.gd" ) @@ -43,6 +49,14 @@ const PROP_MINIMUM_GROUND_CLEARANCE := 0.05 const PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE := 0.35 const PROP_CHANCE_SCALE := 10000 const PROP_SELECTION_WEIGHT_SCALE := 1000 +const WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE := 0.05 +const WATER_RECOVERY_MINIMUM_UP_DOT := 0.6 +const WATER_RECOVERY_FOOTPRINT_RADIUS := 0.6 +const WATER_RECOVERY_INSET_STEP := 0.25 +const WATER_RECOVERY_MAXIMUM_INSET := 1.5 +const WATER_RECOVERY_SEARCH_EXPANSIONS: Array[float] = [1.5, 4.0, 12.0] +const HOME_GROUND_HEIGHT_TOLERANCE := 0.5 +const HOME_ELEVATION_HEIGHT_TOLERANCE := 0.6 const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [ Vector2(1.0, 0.0), Vector2(0.70710678, 0.70710678), @@ -74,8 +88,16 @@ const PROCEDURAL_PROP_GROUPS: Array[StringName] = [ @onready var _player_storage: PlayerStorageInteractionType = ( $Interactables/PlayerStorageBox/InteractionArea ) +@onready var _decor_shop: DecorShopInteractionType = ( + $Interactables/DecorShopWorld/InteractionArea +) +@onready var _rv_upgrade_shop: RVUpgradeInteractionType = ( + $Interactables/RVUpgradeShopWorld/InteractionArea +) @onready var _shop_root: Node3D = %FishingShopWorld @onready var _storage_root: Node3D = %PlayerStorageBox +@onready var _decor_shop_root: Node3D = %DecorShopWorld +@onready var _rv_upgrade_shop_root: Node3D = %RVUpgradeShopWorld @onready var _decorations: Node3D = %Decorations @onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks @onready var _diggable_beach: DiggableArea3D = %DiggableBeach @@ -90,6 +112,8 @@ var _placed_prop_clearance_radii: Array[float] = [] var _placed_prop_groups: Array[StringName] = [] var _placed_group_coordinates: Dictionary[StringName, Array] = {} var _biome_assignments: Dictionary[Vector2i, StringName] = {} +var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {} +var _home_accessible_elevations_by_coordinate: Dictionary[Vector2i, Array] = {} func _ready() -> void: @@ -141,6 +165,14 @@ func get_player_storage() -> PlayerStorageInteractionType: return _player_storage +func get_decor_shop() -> DecorShopInteractionType: + return _decor_shop + + +func get_rv_upgrade_shop() -> RVUpgradeInteractionType: + return _rv_upgrade_shop + + func get_saltwater_shoreline_mesh() -> MeshInstance3D: return _shoreline_reference @@ -166,6 +198,7 @@ func get_spawn_surface_triangles( material_names: Array[StringName], minimum_global_y: float, minimum_up_dot: float = 0.6, + maximum_global_y: float = 100.0, ) -> Array[PackedVector3Array]: var triangles: Array[PackedVector3Array] = [] if material_names.is_empty(): @@ -186,12 +219,62 @@ func get_spawn_surface_triangles( mesh.surface_get_arrays(surface_index), minimum_global_y, minimum_up_dot, + maximum_global_y, ) return triangles +func is_home_exterior_position_accessible(position: Vector3) -> bool: + var local_position := _generator.to_local(position) + if local_position.y <= HOME_GROUND_HEIGHT_TOLERANCE: + return true + var coordinate := _generator.coordinate_for_local_position(local_position) + for value: Variant in _home_accessible_elevations_by_coordinate.get( + coordinate, + [], + ): + if absf(local_position.y - float(value)) <= ( + HOME_ELEVATION_HEIGHT_TOLERANCE + ): + return true + return false + + +func get_water_recovery_position( + entry_position: Vector3, + fallback_position: Vector3, +) -> Vector3: + if ( + not entry_position.is_finite() + or _water_recovery_triangles_by_coordinate.is_empty() + ): + return super(entry_position, fallback_position) + var nearest_surface_distance := _nearest_recovery_surface_distance( + entry_position + ) + if not is_finite(nearest_surface_distance): + return super(entry_position, fallback_position) + for expansion: float in WATER_RECOVERY_SEARCH_EXPANSIONS: + var maximum_distance := nearest_surface_distance + expansion + var result: Variant = _nearest_safe_recovery_surface_position( + entry_position, + maximum_distance, + ) + if result is Vector3: + return result as Vector3 + var map_wide_result: Variant = _nearest_safe_recovery_surface_position( + entry_position, + get_playable_half_extents().length() * 2.0, + ) + if map_wide_result is Vector3: + return map_wide_result as Vector3 + return super(entry_position, fallback_position) + + func _on_generation_completed(summary: Dictionary) -> void: + _diggable_beach.invalidate_surface_cache() var records: Array[Dictionary] = _generator.placement_records() + _cache_home_accessible_elevations(records) _assign_biomes(records, summary) var spawn_position := Vector3.ZERO _clear_children(_decorations) @@ -324,6 +407,10 @@ func _on_generation_completed(summary: Dictionary) -> void: grass_surface_triangles_by_coordinate, sand_surface_triangles_by_coordinate, ) + _cache_water_recovery_surfaces( + grass_surface_triangles_by_coordinate, + sand_surface_triangles_by_coordinate, + ) _place_spawn_amenities(spawn_position) _configure_fresh_water(records) _configure_diggable_area() @@ -334,6 +421,97 @@ func _on_generation_completed(summary: Dictionary) -> void: world_generated.emit(_current_seed, summary) +func _cache_home_accessible_elevations( + records: Array[Dictionary], +) -> void: + _home_accessible_elevations_by_coordinate.clear() + var records_by_coordinate: Dictionary[Vector2i, Dictionary] = {} + var elevation_tiers: Dictionary[int, bool] = {} + for record: Dictionary in records: + var coordinate: Vector2i = record.get( + "coordinate", + Vector2i(-1, -1), + ) + records_by_coordinate[coordinate] = record + for tier: int in _elevation_tiers_for_tags( + record.get("tags", PackedStringArray()) + ): + elevation_tiers[tier] = true + var visited: Dictionary[String, bool] = {} + for tier: int in elevation_tiers: + var elevation_tag := "elevation_%d" % tier + for coordinate: Vector2i in records_by_coordinate: + var visit_key := _home_elevation_visit_key(coordinate, tier) + if visited.has(visit_key): + continue + var record: Dictionary = records_by_coordinate[coordinate] + var tags: PackedStringArray = record.get( + "tags", + PackedStringArray(), + ) + if elevation_tag not in tags: + continue + var component: Array[Vector2i] = [] + var pending: Array[Vector2i] = [coordinate] + var has_ramp := false + while not pending.is_empty(): + var current: Vector2i = pending.pop_back() + var current_key := _home_elevation_visit_key(current, tier) + if visited.has(current_key): + continue + var current_record: Dictionary = records_by_coordinate.get( + current, + {}, + ) + var current_tags: PackedStringArray = current_record.get( + "tags", + PackedStringArray(), + ) + if elevation_tag not in current_tags: + continue + visited[current_key] = true + component.append(current) + has_ramp = has_ramp or "ramp" in current_tags + for offset: Vector2i in [ + Vector2i.LEFT, + Vector2i.RIGHT, + Vector2i.UP, + Vector2i.DOWN, + ]: + pending.append(current + offset) + if not has_ramp: + continue + var surface_height := ( + float(tier - 1) * _generator.elevated_cliff_level_height + ) + for component_coordinate: Vector2i in component: + var heights: Array = ( + _home_accessible_elevations_by_coordinate.get( + component_coordinate, + [], + ) + ) + heights.append(surface_height) + _home_accessible_elevations_by_coordinate[ + component_coordinate + ] = heights + + +func _elevation_tiers_for_tags(tags: PackedStringArray) -> Array[int]: + var tiers: Array[int] = [] + for tag_value: String in tags: + if not tag_value.begins_with("elevation_"): + continue + var suffix := tag_value.trim_prefix("elevation_") + if suffix.is_valid_int(): + tiers.append(suffix.to_int()) + return tiers + + +func _home_elevation_visit_key(coordinate: Vector2i, tier: int) -> String: + return "%d:%d:%d" % [tier, coordinate.x, coordinate.y] + + func _mark_precipitation_occluders() -> void: for prop: Node in _decorations.get_children(): var prop_group := prop.get_meta(&"terrain_prop_group", &"") as StringName @@ -385,6 +563,10 @@ func _place_spawn_amenities(center: Vector3) -> void: _shop_root.rotation.y = PI _storage_root.position = center + Vector3(-2.4, 0.0, 1.2) _storage_root.rotation.y = PI * 0.5 + _decor_shop_root.position = center + Vector3(-2.4, 0.0, 1.2) + _decor_shop_root.rotation.y = PI * 0.5 + _rv_upgrade_shop_root.position = center + Vector3(-2.35, 0.0, -1.35) + _rv_upgrade_shop_root.rotation.y = PI func _configure_static_water() -> void: @@ -453,6 +635,7 @@ func _configure_fresh_water(records: Array[Dictionary]) -> void: PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN ) body.recovery_entry_depth_threshold = 0.1 + body.recovery_entry_confirmation_seconds = 0.2 body.fish_pool = _fresh_water_pool(tags) body.location_tags = _fresh_water_location_tags(tags) body.selection_priority = 10 @@ -1247,6 +1430,7 @@ func _terrain_surface_triangles_by_coordinate( mesh_instance.mesh.surface_get_arrays(surface_index), -INF, 0.35, + 100.0, ) if triangles.is_empty(): continue @@ -1259,6 +1443,203 @@ func _terrain_surface_triangles_by_coordinate( return triangles_by_coordinate +func _cache_water_recovery_surfaces( + grass_triangles: Dictionary[Vector2i, Array], + sand_triangles: Dictionary[Vector2i, Array], +) -> void: + _water_recovery_triangles_by_coordinate.clear() + _append_water_recovery_surfaces(grass_triangles) + _append_water_recovery_surfaces(sand_triangles) + + +func _append_water_recovery_surfaces( + source: Dictionary[Vector2i, Array], +) -> void: + for coordinate: Vector2i in source: + var eligible: Array = _water_recovery_triangles_by_coordinate.get( + coordinate, + [], + ) + for triangle: PackedVector3Array in source[coordinate]: + if triangle.size() != 3: + continue + if minf( + triangle[0].y, + minf(triangle[1].y, triangle[2].y), + ) <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE: + continue + var cross := (triangle[1] - triangle[0]).cross( + triangle[2] - triangle[0] + ) + if ( + cross.length_squared() <= 0.0000001 + or absf(cross.normalized().dot(Vector3.UP)) + < WATER_RECOVERY_MINIMUM_UP_DOT + ): + continue + eligible.append(triangle) + if not eligible.is_empty(): + _water_recovery_triangles_by_coordinate[coordinate] = eligible + + +func _nearest_recovery_surface_distance(entry_position: Vector3) -> float: + var entry_horizontal := Vector2(entry_position.x, entry_position.z) + var nearest_distance_squared := INF + for coordinate: Vector2i in _water_recovery_triangles_by_coordinate: + for triangle: PackedVector3Array in ( + _water_recovery_triangles_by_coordinate[coordinate] + ): + var closest := _closest_horizontal_point_on_triangle( + entry_horizontal, + triangle, + ) + nearest_distance_squared = minf( + nearest_distance_squared, + entry_horizontal.distance_squared_to(closest), + ) + return sqrt(nearest_distance_squared) + + +func _nearest_safe_recovery_surface_position( + entry_position: Vector3, + maximum_distance: float, +) -> Variant: + var entry_horizontal := Vector2(entry_position.x, entry_position.z) + var maximum_distance_squared := maximum_distance * maximum_distance + var best_distance_squared := INF + var best_position: Variant = null + for coordinate: Vector2i in _water_recovery_triangles_by_coordinate: + for triangle: PackedVector3Array in ( + _water_recovery_triangles_by_coordinate[coordinate] + ): + var closest := _closest_horizontal_point_on_triangle( + entry_horizontal, + triangle, + ) + if ( + entry_horizontal.distance_squared_to(closest) + > maximum_distance_squared + ): + continue + var centroid := Vector2( + (triangle[0].x + triangle[1].x + triangle[2].x) / 3.0, + (triangle[0].z + triangle[1].z + triangle[2].z) / 3.0, + ) + var inset_distance := minf( + closest.distance_to(centroid), + WATER_RECOVERY_MAXIMUM_INSET, + ) + var inset_steps := ceili( + inset_distance / WATER_RECOVERY_INSET_STEP + ) + for inset_index: int in inset_steps + 1: + var candidate_horizontal := closest.move_toward( + centroid, + minf( + float(inset_index) * WATER_RECOVERY_INSET_STEP, + inset_distance, + ), + ) + var distance_squared := entry_horizontal.distance_squared_to( + candidate_horizontal + ) + if ( + distance_squared > maximum_distance_squared + or distance_squared >= best_distance_squared + ): + continue + var candidate := Vector3( + candidate_horizontal.x, + entry_position.y, + candidate_horizontal.y, + ) + var safe_position: Variant = _safe_recovery_surface_position( + candidate + ) + if safe_position is Vector3: + best_distance_squared = distance_squared + best_position = safe_position + return best_position + + +func _safe_recovery_surface_position(candidate: Vector3) -> Variant: + var nearby_triangles := _water_recovery_surface_triangles_near(candidate) + var surface_height := _surface_height_at(candidate, nearby_triangles) + if ( + surface_height <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE + or not _surface_supports_prop_footprint( + candidate, + surface_height, + WATER_RECOVERY_FOOTPRINT_RADIUS, + nearby_triangles, + ) + or not _has_prop_clearance( + candidate, + WATER_RECOVERY_FOOTPRINT_RADIUS, + ) + ): + return null + candidate.y = surface_height + return candidate + + +func _water_recovery_surface_triangles_near( + position: Vector3, +) -> Array[PackedVector3Array]: + var result: Array[PackedVector3Array] = [] + if _generator.catalog == null or _generator.catalog.chunk_size <= 0.0: + return result + var local_position := _generator.to_local(position) + var half_grid := Vector2( + float(_generator.grid_size.x - 1) * 0.5, + float(_generator.grid_size.y - 1) * 0.5, + ) + var coordinate := Vector2i( + roundi(local_position.x / _generator.catalog.chunk_size + half_grid.x), + roundi(local_position.z / _generator.catalog.chunk_size + half_grid.y), + ) + for offset_x: int in range(-1, 2): + for offset_y: int in range(-1, 2): + var nearby_coordinate := coordinate + Vector2i(offset_x, offset_y) + result.append_array( + _water_recovery_triangles_by_coordinate.get( + nearby_coordinate, + [], + ) + ) + return result + + +func _closest_horizontal_point_on_triangle( + point: Vector2, + triangle: PackedVector3Array, +) -> Vector2: + var polygon := PackedVector2Array([ + Vector2(triangle[0].x, triangle[0].z), + Vector2(triangle[1].x, triangle[1].z), + Vector2(triangle[2].x, triangle[2].z), + ]) + if Geometry2D.is_point_in_polygon(point, polygon): + return point + var closest := Geometry2D.get_closest_point_to_segment( + point, + polygon[0], + polygon[1], + ) + var nearest_distance_squared := point.distance_squared_to(closest) + for edge_index: int in range(1, 3): + var edge_closest := Geometry2D.get_closest_point_to_segment( + point, + polygon[edge_index], + polygon[(edge_index + 1) % 3], + ) + var distance_squared := point.distance_squared_to(edge_closest) + if distance_squared < nearest_distance_squared: + nearest_distance_squared = distance_squared + closest = edge_closest + return closest + + func _terrain_coordinate_for(mesh_instance: MeshInstance3D) -> Vector2i: var current: Node = mesh_instance while current != null and current != _generator: @@ -1397,6 +1778,7 @@ func _append_surface_triangles( arrays: Array, minimum_global_y: float, minimum_up_dot: float, + maximum_global_y: float, ) -> void: if arrays.size() <= Mesh.ARRAY_INDEX: return @@ -1406,10 +1788,28 @@ func _append_surface_triangles( return if indices.is_empty(): for index: int in range(0, vertices.size() - 2, 3): - _append_triangle(result, mesh_instance, vertices[index], vertices[index + 1], vertices[index + 2], minimum_global_y, minimum_up_dot) + _append_triangle( + result, + mesh_instance, + vertices[index], + vertices[index + 1], + vertices[index + 2], + minimum_global_y, + minimum_up_dot, + maximum_global_y, + ) return for index: int in range(0, indices.size() - 2, 3): - _append_triangle(result, mesh_instance, vertices[indices[index]], vertices[indices[index + 1]], vertices[indices[index + 2]], minimum_global_y, minimum_up_dot) + _append_triangle( + result, + mesh_instance, + vertices[indices[index]], + vertices[indices[index + 1]], + vertices[indices[index + 2]], + minimum_global_y, + minimum_up_dot, + maximum_global_y, + ) func _append_triangle( @@ -1420,11 +1820,15 @@ func _append_triangle( local_c: Vector3, minimum_global_y: float, minimum_up_dot: float, + maximum_global_y: float, ) -> void: var a := mesh_instance.to_global(local_a) var b := mesh_instance.to_global(local_b) var c := mesh_instance.to_global(local_c) - if minf(a.y, minf(b.y, c.y)) <= minimum_global_y: + if ( + maxf(a.y, maxf(b.y, c.y)) <= minimum_global_y + or minf(a.y, minf(b.y, c.y)) >= maximum_global_y + ): return var cross := (b - a).cross(c - a) if cross.length_squared() <= 0.0000001: diff --git a/world/generation/generated_world_region.tscn b/world/generation/generated_world_region.tscn index 47ee6bc..c996b88 100644 --- a/world/generation/generated_world_region.tscn +++ b/world/generation/generated_world_region.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=14 format=3] +[gd_scene load_steps=16 format=3] [ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"] [ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"] @@ -13,6 +13,8 @@ [ext_resource type="Resource" path="res://fish/pools/starter_pond_pool.tres" id="11_pond_pool"] [ext_resource type="Resource" path="res://world/generation/props/terrain_prop_catalog.tres" id="12_prop_catalog"] [ext_resource type="Resource" path="res://world/generation/biomes/terrain_biome_catalog.tres" id="13_biome_catalog"] +[ext_resource type="PackedScene" path="res://world/interactables/decor_shop_world.tscn" id="14_decor_shop"] +[ext_resource type="PackedScene" path="res://world/interactables/rv_upgrade_shop_world.tscn" id="15_rv_upgrade"] [node name="GeneratedWorldRegion" type="Node3D"] script = ExtResource("1_region") @@ -98,6 +100,12 @@ unique_name_in_owner = true [node name="PlayerStorageBox" parent="Interactables" instance=ExtResource("6_storage")] unique_name_in_owner = true +[node name="DecorShopWorld" parent="Interactables" instance=ExtResource("14_decor_shop")] +unique_name_in_owner = true + +[node name="RVUpgradeShopWorld" parent="Interactables" instance=ExtResource("15_rv_upgrade")] +unique_name_in_owner = true + [node name="Decorations" type="Node3D" parent="."] unique_name_in_owner = true diff --git a/world/generation/props/definitions/prop_palm.tres b/world/generation/props/definitions/prop_palm.tres index e586f3d..c8021d4 100644 --- a/world/generation/props/definitions/prop_palm.tres +++ b/world/generation/props/definitions/prop_palm.tres @@ -11,7 +11,7 @@ packed_scene = ExtResource("2_scene") procedural_group = &"sand_tree" required_chunk_tags = PackedStringArray("sand") minimum_spawn_chunk_distance = 2 -clearance_radius = 0.65 +clearance_radius = 0.75 minimum_visual_scale = 0.5 maximum_visual_scale = 1.0 minimum_cluster_size = 2 @@ -21,6 +21,7 @@ maximum_cluster_radius = 3.0 prefer_ocean_facing = true local_overhang_direction = Vector2(-0.883, 0.469) ocean_facing_spread_degrees = 55.0 -collision_radius = 0.4 +collision_radius = 0.7 collision_height = 6.0 +collision_offset = Vector3(0.15, 0, -0.1) gatherable_surface_material_names = PackedStringArray("wood_light") diff --git a/world/generation/props/definitions/prop_pine.tres b/world/generation/props/definitions/prop_pine.tres index b3224a9..5dd460a 100644 --- a/world/generation/props/definitions/prop_pine.tres +++ b/world/generation/props/definitions/prop_pine.tres @@ -15,5 +15,6 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.55 minimum_visual_scale = 0.65 maximum_visual_scale = 1.2 -collision_radius = 0.5 +collision_radius = 0.85 collision_height = 4.0 +collision_offset = Vector3(-0.044, 0, 0.083) diff --git a/world/generation/props/definitions/prop_pine_large.tres b/world/generation/props/definitions/prop_pine_large.tres index df9076d..bc70868 100644 --- a/world/generation/props/definitions/prop_pine_large.tres +++ b/world/generation/props/definitions/prop_pine_large.tres @@ -15,5 +15,6 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.65 minimum_visual_scale = 0.75 maximum_visual_scale = 1.2 -collision_radius = 0.65 +collision_radius = 0.75 collision_height = 9.5 +collision_offset = Vector3(-0.04, 0, 0.075) diff --git a/world/generation/props/definitions/prop_tree_1.tres b/world/generation/props/definitions/prop_tree_1.tres index 3553988..4688578 100644 --- a/world/generation/props/definitions/prop_tree_1.tres +++ b/world/generation/props/definitions/prop_tree_1.tres @@ -27,4 +27,5 @@ secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) collision_radius = 0.4 collision_height = 3.2 +collision_offset = Vector3(0.08, 0, 0.01) gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark") diff --git a/world/generation/props/definitions/prop_tree_2.tres b/world/generation/props/definitions/prop_tree_2.tres index 868e044..a6e7895 100644 --- a/world/generation/props/definitions/prop_tree_2.tres +++ b/world/generation/props/definitions/prop_tree_2.tres @@ -25,6 +25,7 @@ variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid") secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) -collision_radius = 0.5 +collision_radius = 0.55 collision_height = 3.8 +collision_offset = Vector3(0.1, 0, 0.075) gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark") diff --git a/world/generation/props/definitions/prop_tree_3.tres b/world/generation/props/definitions/prop_tree_3.tres index 58e3549..9f833c9 100644 --- a/world/generation/props/definitions/prop_tree_3.tres +++ b/world/generation/props/definitions/prop_tree_3.tres @@ -27,4 +27,5 @@ secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) collision_radius = 0.55 collision_height = 4.2 +collision_offset = Vector3(-0.1, 0, 0.18) gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark") diff --git a/world/generation/props/definitions/prop_tree_large.tres b/world/generation/props/definitions/prop_tree_large.tres index 9092ad1..ee91d62 100644 --- a/world/generation/props/definitions/prop_tree_large.tres +++ b/world/generation/props/definitions/prop_tree_large.tres @@ -25,6 +25,7 @@ variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid") secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) -collision_radius = 0.85 +collision_radius = 1.05 collision_height = 7.5 +collision_offset = Vector3(0.084, 0, 0.504) gatherable_surface_material_names = PackedStringArray("wood", "wood_light", "wood_mid", "wood_dark") diff --git a/world/generation/terrain_chunk_generator.gd b/world/generation/terrain_chunk_generator.gd index 49bf693..64a8bd2 100644 --- a/world/generation/terrain_chunk_generator.gd +++ b/world/generation/terrain_chunk_generator.gd @@ -4414,6 +4414,19 @@ func chunk_position(coordinate: Vector2i) -> Vector3: ) +func coordinate_for_local_position(position: Vector3) -> Vector2i: + if catalog == null or catalog.chunk_size <= 0.0: + return Vector2i(-1, -1) + var half_grid := Vector2( + float(grid_size.x - 1) * 0.5, + float(grid_size.y - 1) * 0.5, + ) + return Vector2i( + roundi(position.x / catalog.chunk_size + half_grid.x), + roundi(position.z / catalog.chunk_size + half_grid.y), + ) + + func get_generated_chunks_root() -> Node3D: return _generated_chunks diff --git a/world/interactables/decor_shop_world.tscn b/world/interactables/decor_shop_world.tscn new file mode 100644 index 0000000..7f94ec4 --- /dev/null +++ b/world/interactables/decor_shop_world.tscn @@ -0,0 +1,35 @@ +[gd_scene load_steps=5 format=3] + +[ext_resource type="Script" path="res://world/decor_shop_interaction.gd" id="1_shop"] +[ext_resource type="PackedScene" path="res://art/exported/characters/base/straywild_base_character.glb" id="2_character"] +[ext_resource type="Script" path="res://world/world_character_display.gd" id="3_display"] + +[sub_resource type="SphereShape3D" id="InteractionShape"] +resource_local_to_scene = true +radius = 2.1 + +[node name="DecorShopWorld" type="Node3D"] + +[node name="Shopkeeper" parent="." instance=ExtResource("2_character")] +script = ExtResource("3_display") +species_id = "round" +fur_color_id = "brown" +ears_id = "bear" +tail_id = "bear" +eyes_id = "tired" +nose_id = "triangle_pink" +mouth_id = "simple_smile" +head_look_enabled = true + +[node name="AnimationPlayer" parent="Shopkeeper"] +autoplay = "idle" + +[node name="InteractionArea" type="Area3D" parent="."] +unique_name_in_owner = true +position = Vector3(0, 1, -0.3) +collision_layer = 0 +collision_mask = 2 +script = ExtResource("1_shop") + +[node name="InteractionShape" type="CollisionShape3D" parent="InteractionArea"] +shape = SubResource("InteractionShape") diff --git a/world/interactables/fishing_shop_world.tscn b/world/interactables/fishing_shop_world.tscn index 7221005..040b558 100644 --- a/world/interactables/fishing_shop_world.tscn +++ b/world/interactables/fishing_shop_world.tscn @@ -1,12 +1,12 @@ [gd_scene load_steps=5 format=3] [ext_resource type="Script" path="res://world/fishing_shop_interaction.gd" id="1_shop"] -[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="2_character"] +[ext_resource type="PackedScene" path="res://art/exported/characters/base/straywild_base_character.glb" id="2_character"] [ext_resource type="Script" path="res://world/world_character_display.gd" id="3_display"] [sub_resource type="SphereShape3D" id="InteractionShape"] resource_local_to_scene = true -radius = 3.0 +radius = 2.1 [node name="FishingShopWorld" type="Node3D"] diff --git a/world/interactables/player_storage_box.tscn b/world/interactables/player_storage_box.tscn index 4bf9a8e..008f0ac 100644 --- a/world/interactables/player_storage_box.tscn +++ b/world/interactables/player_storage_box.tscn @@ -27,6 +27,8 @@ size = Vector3(1.62, 0.86, 1.02) radius = 2.3 [node name="PlayerStorageBox" type="Node3D"] +process_mode = 4 +visible = false [node name="Body" type="MeshInstance3D" parent="."] position = Vector3(0, 0.35, 0) @@ -44,6 +46,7 @@ collision_mask = 0 [node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"] position = Vector3(0, 0.43, 0) +disabled = true shape = SubResource("CollisionShape") [node name="InteractionArea" type="Area3D" parent="."] @@ -54,6 +57,7 @@ collision_mask = 2 script = ExtResource("1_interaction") [node name="CollisionShape3D" type="CollisionShape3D" parent="InteractionArea"] +disabled = true shape = SubResource("InteractionShape") [node name="PromptAnchor" type="Marker3D" parent="."] diff --git a/world/interactables/rv_upgrade_shop_world.tscn b/world/interactables/rv_upgrade_shop_world.tscn new file mode 100644 index 0000000..9fd0ed2 --- /dev/null +++ b/world/interactables/rv_upgrade_shop_world.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=5 format=3] + +[ext_resource type="Script" path="res://world/rv_upgrade_interaction.gd" id="1_shop"] +[ext_resource type="PackedScene" path="res://art/exported/characters/base/straywild_base_character.glb" id="2_character"] +[ext_resource type="Script" path="res://world/world_character_display.gd" id="3_display"] + +[sub_resource type="SphereShape3D" id="InteractionShape"] +resource_local_to_scene = true +radius = 2.1 + +[node name="RVUpgradeShopWorld" type="Node3D"] + +[node name="Shopkeeper" parent="." instance=ExtResource("2_character")] +script = ExtResource("3_display") +species_id = "pointy" +fur_color_id = "teal" +ears_id = "pointy_long" +tail_id = "fox" +eyes_id = "interested" +nose_id = "triangle_small" +mouth_id = "cheeky_smile" +head_look_enabled = true + +[node name="AnimationPlayer" parent="Shopkeeper"] +autoplay = "idle" + +[node name="InteractionArea" type="Area3D" parent="."] +unique_name_in_owner = true +position = Vector3(0, 1, -0.3) +collision_layer = 0 +collision_mask = 2 +script = ExtResource("1_shop") + +[node name="InteractionShape" type="CollisionShape3D" parent="InteractionArea"] +shape = SubResource("InteractionShape") +vertical_icon_alignment = 1 diff --git a/world/player_water_trigger.gd b/world/player_water_trigger.gd index 10fab14..ffe0582 100644 --- a/world/player_water_trigger.gd +++ b/world/player_water_trigger.gd @@ -29,9 +29,14 @@ enum EntryHeightReference { ) ## Recovery begins when the selected reference reaches this depth. @export_range(0.0, 2.0, 0.05) var entry_depth_threshold: float = 0.35 +## The player must remain below the threshold for this long before recovery. +## A short freshwater confirmation keeps jumps across stepping stones from +## being mistaken for a completed fall into the water. +@export_range(0.0, 1.0, 0.05) var entry_confirmation_seconds: float = 0.0 var _tracked_players: Array[Player] = [] var _triggered_players: Dictionary[StringName, bool] = {} +var _submerged_seconds: Dictionary[StringName, float] = {} func _ready() -> void: @@ -46,7 +51,7 @@ func _ready() -> void: set_physics_process(false) -func _physics_process(_delta: float) -> void: +func _physics_process(delta: float) -> void: var active_surface_height: float = get_surface_height() for player: Player in _tracked_players.duplicate(): if not is_instance_valid(player): @@ -58,9 +63,17 @@ func _physics_process(_delta: float) -> void: var entry_height := player.get_body_center_position().y if entry_height_reference == EntryHeightReference.PLAYER_ORIGIN: entry_height = player.global_position.y - if entry_height <= active_surface_height - entry_depth_threshold: - _triggered_players[player_key] = true - recovery_requested.emit(player, active_surface_height) + if entry_height > active_surface_height - entry_depth_threshold: + _submerged_seconds[player_key] = 0.0 + continue + var submerged_seconds: float = ( + _submerged_seconds.get(player_key, 0.0) + maxf(delta, 0.0) + ) + _submerged_seconds[player_key] = submerged_seconds + if submerged_seconds < entry_confirmation_seconds: + continue + _triggered_players[player_key] = true + recovery_requested.emit(player, active_surface_height) if _tracked_players.is_empty(): set_physics_process(false) @@ -86,6 +99,7 @@ func _on_body_entered(body: Node3D) -> void: if player == null or player in _tracked_players: return _tracked_players.append(player) + _submerged_seconds[StringName(str(player.get_instance_id()))] = 0.0 set_physics_process(true) @@ -94,6 +108,8 @@ func _on_body_exited(body: Node3D) -> void: if player == null: return _tracked_players.erase(player) - _triggered_players.erase(StringName(str(player.get_instance_id()))) + var player_key := StringName(str(player.get_instance_id())) + _triggered_players.erase(player_key) + _submerged_seconds.erase(player_key) if _tracked_players.is_empty(): set_physics_process(false) diff --git a/world/regions/starter_island_region.gd b/world/regions/starter_island_region.gd index 3af3010..c690e4b 100644 --- a/world/regions/starter_island_region.gd +++ b/world/regions/starter_island_region.gd @@ -8,6 +8,12 @@ const FishingShopInteractionType = preload( const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const FOLIAGE_WIND_SHADER: Shader = preload( "res://world/materials/foliage_wind.gdshader" ) @@ -49,6 +55,14 @@ var fishing_shop_path: NodePath = ( var player_storage_path: NodePath = ( ^"Interactables/PlayerStorageBox/InteractionArea" ) +@export_node_path("Area3D") +var decor_shop_path: NodePath = ( + ^"Interactables/DecorShopWorld/InteractionArea" +) +@export_node_path("Area3D") +var rv_upgrade_shop_path: NodePath = ( + ^"Interactables/RVUpgradeShopWorld/InteractionArea" +) @export_group("Terrain Collision") # The imported GLB is the collision authority. Rebuild once per region load so # newly authored terrain and props cannot retain a stale saved fallback shape. @@ -84,6 +98,14 @@ func get_player_storage() -> PlayerStorageInteractionType: return get_node_or_null(player_storage_path) as PlayerStorageInteractionType +func get_decor_shop() -> DecorShopInteractionType: + return get_node_or_null(decor_shop_path) as DecorShopInteractionType + + +func get_rv_upgrade_shop() -> RVUpgradeInteractionType: + return get_node_or_null(rv_upgrade_shop_path) as RVUpgradeInteractionType + + func get_saltwater_shoreline_mesh() -> MeshInstance3D: return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D @@ -139,6 +161,7 @@ func get_spawn_surface_triangles( material_names: Array[StringName], minimum_global_y: float, minimum_up_dot: float = 0.6, + maximum_global_y: float = 100.0, ) -> Array[PackedVector3Array]: var triangles: Array[PackedVector3Array] = [] if material_names.is_empty(): @@ -177,6 +200,7 @@ func get_spawn_surface_triangles( vertices[vertex_index + 2], minimum_global_y, minimum_up_dot, + maximum_global_y, ) else: for index_offset: int in range(0, indices.size() - 2, 3): @@ -188,6 +212,7 @@ func get_spawn_surface_triangles( vertices[indices[index_offset + 2]], minimum_global_y, minimum_up_dot, + maximum_global_y, ) return triangles @@ -200,14 +225,14 @@ func _append_spawn_triangle( local_c: Vector3, minimum_global_y: float, minimum_up_dot: float, + maximum_global_y: float, ) -> void: var a: Vector3 = mesh_instance.to_global(local_a) var b: Vector3 = mesh_instance.to_global(local_b) var c: Vector3 = mesh_instance.to_global(local_c) if ( - a.y <= minimum_global_y - or b.y <= minimum_global_y - or c.y <= minimum_global_y + maxf(a.y, maxf(b.y, c.y)) <= minimum_global_y + or minf(a.y, minf(b.y, c.y)) >= maximum_global_y ): return var cross: Vector3 = (b - a).cross(c - a) diff --git a/world/regions/starter_island_region.tscn b/world/regions/starter_island_region.tscn index b52b0ea..7682491 100644 --- a/world/regions/starter_island_region.tscn +++ b/world/regions/starter_island_region.tscn @@ -18,6 +18,8 @@ [ext_resource type="Script" path="res://world/water/shoreline_ribbon_config.gd" id="22_shoreline_config"] [ext_resource type="Script" path="res://world/digging/diggable_area_3d.gd" id="23_diggable_area"] [ext_resource type="Script" path="res://world/gathering/gatherable_anchor_set_3d.gd" id="24_gatherable_anchors"] +[ext_resource type="PackedScene" path="res://world/interactables/decor_shop_world.tscn" id="25_decor_shop"] +[ext_resource type="PackedScene" path="res://world/interactables/rv_upgrade_shop_world.tscn" id="26_rv_upgrade"] [sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_4e4k5"] data = PackedVector3Array(10.033899, 3.3913999, 3.3446, 13.3786, 3.3913999, 0, 13.3786, 3.3913999, 3.3446, 10.033899, 3.3913999, 3.3446, 10.033899, 3.3913999, 0, 13.3786, 3.3913999, 0, 6.6893, 3.3913999, 3.3446, 10.033899, 3.3913999, 0, 10.033899, 3.3913999, 3.3446, 6.6893, 3.3913999, 3.3446, 6.6893, 3.3913999, 0, 10.033899, 3.3913999, 0, 6.6893, 3.3913999, 0, 10.033899, 3.3913999, -3.3446, 10.033899, 3.3913999, 0, 6.6893, 3.3913999, 6.6893, 6.6893, 3.3913999, 3.3446, 10.033899, 3.3913999, 3.3446, 6.6893, 3.3913999, 6.6893, 10.033899, 3.3913999, 3.3446, 10.033899, 3.3913999, 6.6893, 3.3446, 3.3913999, 3.3446, 6.6893, 3.3913999, 0, 6.6893, 3.3913999, 3.3446, 6.6893, 3.3913999, 10.033899, 6.6893, 3.3913999, 6.6893, 10.033899, 3.3913999, 6.6893, 6.6893, 3.3913999, 10.033899, 10.033899, 3.3913999, 6.6893, 10.033899, 3.3913999, 10.033899, 3.3446, 3.3913999, 6.6893, 6.6893, 3.3913999, 3.3446, 6.6893, 3.3913999, 6.6893, 3.3446, 3.3913999, 6.6893, 3.3446, 3.3913999, 3.3446, 6.6893, 3.3913999, 3.3446, 3.3446, 3.3913999, 10.033899, 6.6893, 3.3913999, 6.6893, 6.6893, 3.3913999, 10.033899, 3.3446, 3.3913999, 10.033899, 3.3446, 3.3913999, 6.6893, 6.6893, 3.3913999, 6.6893, 0, 3.3913999, 6.6893, 3.3446, 3.3913999, 3.3446, 3.3446, 3.3913999, 6.6893, 0, 3.3913999, 6.6893, 0, 3.3913999, 3.3446, 3.3446, 3.3913999, 3.3446, 0, 3.3913999, 10.033899, 0, 3.3913999, 6.6893, 3.3446, 3.3913999, 6.6893, 0, 3.3913999, 10.033899, 3.3446, 3.3913999, 6.6893, 3.3446, 3.3913999, 10.033899, -3.3446, 3.3913999, 13.3786, -3.3446, 3.3913999, 10.033899, 0, 3.3913999, 10.033899, -3.3446, 3.3913999, 13.3786, 0, 3.3913999, 10.033899, 0, 3.3913999, 13.3786, 0, 3.3913999, 13.3786, 0, 3.3913999, 10.033899, 3.3446, 3.3913999, 10.033899, 0, 3.3913999, 13.3786, 3.3446, 3.3913999, 10.033899, 3.3446, 3.3913999, 13.3786, 3.3446, 3.3913999, 13.3786, 3.3446, 3.3913999, 10.033899, 6.6893, 3.3913999, 10.033899, 3.3446, 3.3913999, 13.3786, 6.6893, 3.3913999, 10.033899, 6.6893, 3.3913999, 13.3786, 3.9148, 3.3913999, 16.514599, 3.3446, 3.3913999, 13.3786, 6.6893, 3.3913999, 13.3786, 3.3446, 3.3913999, 13.3786, 3.9148, 3.3913999, 16.514599, 3.6399, 3.3913999, 16.2405, 3.9148, 3.3913999, 16.514599, 6.6893, 3.3913999, 13.3786, 6.6893, 3.3913999, 16.7232, 6.6893, 3.3913999, 16.7232, 6.6949, 3.3913999, 18.9914, 6.6893, 3.3913999, 18.9861, 6.6949, 3.3913999, 18.9914, 6.6893, 3.3913999, 16.7232, 10.3743, 3.3913999, 16.7232, 6.6949, 3.3913999, 18.9914, 10.3743, 3.3913999, 16.7232, 9.1275, 3.3913999, 18.830599, 10.033899, 3.3913999, -10.033899, 13.3786, 3.3913999, -13.3786, 13.3786, 3.3913999, -10.033899, 13.3786, 3.3913999, -13.3786, 16.7232, 3.3913999, -10.033899, 13.3786, 3.3913999, -10.033899, 16.7232, 3.3913999, -10.033899, 19.798899, 3.3913999, -13.1096, 19.798899, 3.3913999, -10.033899, 16.7232, 3.3913999, -10.033899, 16.7232, 3.3913999, -13.1096, 19.798899, 3.3913999, -13.1096, 16.7232, 3.3913999, -10.033899, 16.4543, 3.3913999, -13.3786, 16.7232, 3.3913999, -13.1096, 16.7232, 3.3913999, -10.033899, 13.3786, 3.3913999, -13.3786, 16.4543, 3.3913999, -13.3786, 13.3786, 3.3913999, -6.6893, 13.3786, 3.3913999, -10.033899, 16.7232, 3.3913999, -10.033899, 13.3786, 3.3913999, -6.6893, 16.7232, 3.3913999, -10.033899, 16.7232, 3.3913999, -6.6893, 10.033899, 3.3913999, -6.6893, 13.3786, 3.3913999, -10.033899, 13.3786, 3.3913999, -6.6893, 10.033899, 3.3913999, -6.6893, 10.033899, 3.3913999, -10.033899, 13.3786, 3.3913999, -10.033899, 16.4543, 3.3913999, -13.3786, 16.495699, 3.3913999, -16.459, 16.495699, 3.3913999, -13.42, 16.4543, 3.3913999, -13.3786, 12.738999, 3.3913999, -15.894899, 16.495699, 3.3913999, -16.459, 12.738999, 3.3913999, -15.894899, 16.4543, 3.3913999, -13.3786, 13.3786, 3.3913999, -13.3786, 13.3786, 3.3913999, -13.3786, 12.460899, 3.3913999, -16.1674, 12.738999, 3.3913999, -15.894899, 13.3786, 3.3913999, -13.3786, 10.033899, 3.3913999, -16.7232, 12.460899, 3.3913999, -16.1674, 10.033899, 3.3913999, -10.033899, 10.033899, 3.3913999, -13.3786, 13.3786, 3.3913999, -13.3786, 10.033899, 3.3913999, -16.7232, 13.3786, 3.3913999, -13.3786, 10.033899, 3.3913999, -13.3786, 6.6893, 3.3913999, -13.3786, 10.033899, 3.3913999, -16.7232, 10.033899, 3.3913999, -13.3786, 6.6893, 3.3913999, -13.3786, 6.6893, 3.3913999, -16.7232, 10.033899, 3.3913999, -16.7232, 6.6893, 3.3913999, -10.033899, 10.033899, 3.3913999, -13.3786, 10.033899, 3.3913999, -10.033899, 6.6893, 3.3913999, -10.033899, 6.6893, 3.3913999, -13.3786, 10.033899, 3.3913999, -13.3786, 6.6893, 3.3913999, -6.6893, 6.6893, 3.3913999, -10.033899, 10.033899, 3.3913999, -10.033899, 6.6893, 3.3913999, -6.6893, 10.033899, 3.3913999, -10.033899, 10.033899, 3.3913999, -6.6893, 4.0225, 3.3913999, -8.7143, 6.6893, 3.3913999, -13.3786, 6.6893, 3.3913999, -10.033899, 4.6821, 3.3913999, -13.3786, 6.6893, 3.3913999, -16.7232, 6.6893, 3.3913999, -13.3786, 4.0225, 3.3913999, -8.7143, 4.6821, 3.3913999, -13.3786, 6.6893, 3.3913999, -13.3786, 4.6821, 3.3913999, -13.3786, 3.8388, 3.3913999, -16.7232, 6.6893, 3.3913999, -16.7232, 4.6821, 3.3913999, -13.3786, 4.0225, 3.3913999, -8.7143, 4.6821, 3.3913999, -13.3786, 4.6821, 3.3913999, -13.3786, 4.6821, 3.3913999, -13.3786, 4.4131, 3.2091, -13.357699, 4.6821, 3.3913999, -13.3786, 4.4131, 3.2091, -13.357699, 4.4131, 3.2091, -13.357699, 3.3446, 3.3913999, -6.6893, 4.0225, 3.3913999, -8.7143, 6.6893, 3.3913999, -10.033899, 3.3446, 3.3913999, -6.6893, 6.6893, 3.3913999, -10.033899, 6.6893, 3.3913999, -6.6893, 0, 3.3913999, -6.6893, 4.0225, 3.3913999, -8.7143, 3.3446, 3.3913999, -6.6893, 0, 3.3913999, -6.6893, 0, 3.3913999, -8.7143, 4.0225, 3.3913999, -8.7143, 3.3446, 3.3913999, -3.3446, 3.3446, 3.3913999, -6.6893, 6.6893, 3.3913999, -6.6893, 0, 3.3913999, -6.6893, -3.9101999, 3.3913999, -9.8592, 0, 3.3913999, -8.7143, 0, 3.3913999, -8.7143, -3.9101999, 3.3913999, -9.8592, 0, 3.3913999, -8.7143, 0, 3.3913999, -3.3446, 0, 3.3913999, -6.6893, 3.3446, 3.3913999, -6.6893, 0, 3.3913999, -3.3446, 3.3446, 3.3913999, -6.6893, 3.3446, 3.3913999, -3.3446, -3.6378, 3.3913999, -2.0792, 0, 3.3913999, -6.6893, 0, 3.3913999, -3.3446, -2.9566998, 3.3913999, 0, -3.6378, 3.3913999, -2.0792, 0, 3.3913999, -3.3446, -3.6378, 3.3913999, -2.0792, -3.3446, 3.3913999, -6.6893, 0, 3.3913999, -6.6893, -3.9101999, 3.3913999, -9.8592, 0, 3.3913999, -6.6893, -3.3446, 3.3913999, -6.6893, -6.6893, 3.3913999, -3.7326, -3.3446, 3.3913999, -6.6893, -3.6378, 3.3913999, -2.0792, -2.9566998, 3.3913999, 0, 0, 3.3913999, -3.3446, 0, 3.3913999, 0, 0, 3.3913999, 0, 0, 3.3913999, -3.3446, 3.3446, 3.3913999, -3.3446, -2.9566998, 3.3913999, 3.3446, -2.9566998, 3.3913999, 0, 0, 3.3913999, 0, -2.9566998, 3.3913999, 3.3446, 0, 3.3913999, 0, 0, 3.3913999, 3.3446, -6.6893, 3.3913999, -3.7326, -6.6893, 3.3913999, -6.6893, -3.3446, 3.3913999, -6.6893, -6.6893, 3.3913999, -6.6893, -3.9101999, 3.3913999, -9.8592, -3.3446, 3.3913999, -6.6893, -10.033899, 3.3913999, -3.7326, -6.6893, 3.3913999, -6.6893, -6.6893, 3.3913999, -3.7326, 0, 3.3913999, 3.3446, 0, 3.3913999, 0, 3.3446, 3.3913999, 0, 0, 3.3913999, 0, 3.3446, 3.3913999, -3.3446, 3.3446, 3.3913999, 0, 0, 3.3913999, 3.3446, 3.3446, 3.3913999, 0, 3.3446, 3.3913999, 3.3446, 3.3446, 3.3913999, 3.3446, 3.3446, 3.3913999, 0, 6.6893, 3.3913999, 0, 3.3446, 3.3913999, 0, 3.3446, 3.3913999, -3.3446, 6.6893, 3.3913999, -3.3446, 3.3446, 3.3913999, 0, 6.6893, 3.3913999, -3.3446, 6.6893, 3.3913999, 0, 3.3446, 3.3913999, -3.3446, 6.6893, 3.3913999, -6.6893, 6.6893, 3.3913999, -3.3446, 6.6893, 3.3913999, 0, 6.6893, 3.3913999, -3.3446, 10.033899, 3.3913999, -3.3446, 6.6893, 3.3913999, -3.3446, 6.6893, 3.3913999, -6.6893, 10.033899, 3.3913999, -6.6893, 6.6893, 3.3913999, -3.3446, 10.033899, 3.3913999, -6.6893, 10.033899, 3.3913999, -3.3446, -6.6893, 3.3913999, -6.6893, -6.6893, 3.3913999, -10.033899, -3.9101999, 3.3913999, -9.8592, -6.6893, 3.3913999, -10.033899, -3.9101999, 3.3913999, -12.538199, -3.9101999, 3.3913999, -9.8592, -10.033899, 3.3913999, -3.7326, -10.033899, 3.3913999, -6.6893, -6.6893, 3.3913999, -6.6893, -10.033899, 3.3913999, -6.6893, -6.6893, 3.3913999, -10.033899, -6.6893, 3.3913999, -6.6893, -13.3786, 3.3913999, -3.7326, -10.033899, 3.3913999, -6.6893, -10.033899, 3.3913999, -3.7326, -6.6893, 3.3913999, -10.033899, -6.6893, 3.3913999, -12.538199, -3.9101999, 3.3913999, -12.538199, -13.3786, 3.3913999, -3.7326, -13.3786, 3.3913999, -6.6893, -10.033899, 3.3913999, -6.6893, -16.191599, 3.3913999, -3.0971, -13.3786, 3.3913999, -6.6893, -13.3786, 3.3913999, -3.7326, -10.033899, 3.3913999, -6.6893, -10.033899, 3.3913999, -10.033899, -6.6893, 3.3913999, -10.033899, -13.3786, 3.3913999, -6.6893, -10.033899, 3.3913999, -10.033899, -10.033899, 3.3913999, -6.6893, -16.191599, 3.3913999, -3.0971, -16.7232, 3.3913999, -6.6893, -13.3786, 3.3913999, -6.6893, -13.3786, 3.3913999, -6.6893, -13.3786, 3.3913999, -10.033899, -10.033899, 3.3913999, -10.033899, -19.7545, 3.3913999, -2.7851999, -16.7232, 3.3913999, -6.6893, -16.191599, 3.3913999, -3.0971, -19.7545, 3.3913999, -2.7851999, -19.280699, 3.3913999, -6.6893, -16.7232, 3.3913999, -6.6893, -17.1112, 3.3913999, 0, -19.7545, 3.3913999, -2.7851999, -16.191599, 3.3913999, -3.0971, -17.1112, 3.3913999, 0, -19.9511, 3.3913999, -2.5834, -19.7545, 3.3913999, -2.7851999, -19.9511, 3.3913999, -2.5834, -17.1112, 3.3913999, 0, -20.0679, 3.3913999, 0, -20.0679, 3.3913999, 3.3446, -20.0679, 3.3913999, 0, -17.1112, 3.3913999, 0, -20.0679, 3.3913999, 3.3446, -17.1112, 3.3913999, 0, -17.1112, 3.3913999, 3.3044999, -13.3786, 3.3913999, -10.033899, -10.699599, 3.3913999, -13.175799, -10.033899, 3.3913999, -10.033899, -10.699599, 3.3913999, -13.175799, -6.6893, 3.3913999, -10.033899, -10.033899, 3.3913999, -10.033899, -13.3786, 3.3913999, -10.033899, -13.3786, 3.3913999, -13.3786, -10.699599, 3.3913999, -13.175799, -6.6893, 3.3913999, -10.033899, -10.699599, 3.3913999, -13.175799, -6.6893, 3.3913999, -12.538199, -13.3786, 3.3913999, -13.3786, -10.699599, 3.3913999, -15.835, -10.699599, 3.3913999, -13.175799, -13.3786, 3.3913999, -13.3786, -13.3786, 3.3913999, -15.4154, -10.699599, 3.3913999, -15.835, -6.6893, 3.3913999, -12.538199, -10.699599, 3.3913999, -13.175799, -6.6893, 3.3913999, -12.538199, -6.6893, 3.3913999, -12.538199, -6.6893, 3.3913999, -12.538199, -6.6893, 3.175, -12.867, -6.6893, 3.3913999, -12.538199, -6.6893, 3.175, -12.867, -6.6893, 3.175, -12.867, -20.0679, 3.3913999, 6.6893, -20.0679, 3.3913999, 3.3446, -17.1112, 3.3913999, 3.3044999, -22.5541, 3.3913999, 9.2058, -22.3151, 3.3913999, 6.6893, -20.0679, 3.3913999, 6.6893, -22.5541, 3.3913999, 9.2058, -20.0679, 3.3913999, 6.6893, -20.0679, 3.3913999, 9.167399, -20.0679, 3.3913999, 9.167399, -20.0679, 3.3913999, 6.6893, -16.7232, 3.3913999, 6.6893, -20.0679, 3.3913999, 6.6893, -17.1112, 3.3913999, 3.3044999, -16.7232, 3.3913999, 6.6893, -20.0679, 3.3913999, 9.167399, -16.7232, 3.3913999, 6.6893, -16.285099, 3.3913999, 9.5865, -16.7232, 3.3913999, 6.6893, -16.053999, 3.3913999, 9.8082, -16.285099, 3.3913999, 9.5865, -16.053999, 3.3913999, 9.8082, -16.7232, 3.3913999, 6.6893, -13.1355, 3.3913999, 6.7549996, -17.1112, 3.3913999, 3.3044999, -13.1355, 3.3913999, 6.7549996, -16.7232, 3.3913999, 6.6893, -16.053999, 3.3913999, 9.8082, -13.1355, 3.3913999, 6.7549996, -13.3786, 3.3913999, 10.033899, -13.1355, 3.3913999, 6.7549996, -17.1112, 3.3913999, 3.3044999, -14.877199, 3.3913999, 4.6657, -14.877199, 3.3913999, 4.6657, -17.1112, 3.3913999, 3.3044999, -15.582299, 3.3913999, 4.0366, -13.1355, 3.3913999, 6.7549996, -14.877199, 3.3913999, 4.6657, -14.0948, 3.3913999, 5.5073, -13.3786, 3.3913999, 10.033899, -13.1355, 3.3913999, 6.7549996, -10.033899, 3.3913999, 7.0772, -13.3786, 3.3913999, 10.033899, -10.033899, 3.3913999, 7.0772, -10.033899, 3.3913999, 10.033899, -10.033899, 3.3913999, 10.033899, -10.033899, 3.3913999, 7.0772, -6.6893, 3.3913999, 7.0772, -10.033899, 3.3913999, 10.033899, -6.6893, 3.3913999, 7.0772, -6.6893, 3.3913999, 10.033899, -10.033899, 3.0033998, 6.6893, -13.1355, 3.3913999, 6.7549996, -12.747499, 3.0033998, 6.3671, -10.033899, 3.0033998, 6.6893, -10.033899, 3.3913999, 7.0772, -13.1355, 3.3913999, 6.7549996, -2.5674999, 2.4164, 7.6215, -6.6893, 3.3913999, 7.0772, -6.6893, 3.0033998, 6.6893, -2.5674999, 2.4164, 7.6215, -2.1794999, 2.8042998, 8.009399, -6.6893, 3.3913999, 7.0772, -12.747499, 3.0033998, 6.3671, -14.0948, 3.3913999, 5.5073, -13.6942, 3.0033998, 5.1798, -12.747499, 3.0033998, 6.3671, -13.1355, 3.3913999, 6.7549996, -14.0948, 3.3913999, 5.5073, -6.6893, 3.0033998, 6.6893, -10.033899, 3.3913999, 7.0772, -10.033899, 3.0033998, 6.6893, -6.6893, 3.0033998, 6.6893, -6.6893, 3.3913999, 7.0772, -10.033899, 3.3913999, 7.0772, -16.7232, 3.0033998, 0, -16.191599, 3.3913999, -3.0971, -15.8036995, 3.0033998, -2.7091, -16.7232, 3.0033998, 0, -17.1112, 3.3913999, 0, -16.191599, 3.3913999, -3.0971, -13.3786, 3.3913999, -3.7326, -15.8036995, 3.0033998, -2.7091, -16.191599, 3.3913999, -3.0971, -13.3786, 3.3913999, -3.7326, -13.3786, 3.0033998, -3.3446, -15.8036995, 3.0033998, -2.7091, -2.9566998, 3.3913999, 0, -4.0257998, 3.0033998, -1.6911999, -3.6378, 3.3913999, -2.0792, -2.9566998, 3.3913999, 0, -3.3446, 3.0033998, 0, -4.0257998, 3.0033998, -1.6911999, -3.6378, 3.3913999, -2.0792, -6.6893, 3.0033998, -3.3446, -6.6893, 3.3913999, -3.7326, -3.6378, 3.3913999, -2.0792, -4.0257998, 3.0033998, -1.6911999, -6.6893, 3.0033998, -3.3446, -10.033899, 3.3913999, -3.7326, -13.3786, 3.0033998, -3.3446, -13.3786, 3.3913999, -3.7326, -10.033899, 3.3913999, -3.7326, -10.033899, 3.0033998, -3.3446, -13.3786, 3.0033998, -3.3446, -6.6893, 3.3913999, -3.7326, -10.033899, 3.0033998, -3.3446, -10.033899, 3.3913999, -3.7326, -6.6893, 3.3913999, -3.7326, -6.6893, 3.0033998, -3.3446, -10.033899, 3.0033998, -3.3446, -16.7232, 3.0033998, 2.9166, -17.1112, 3.3913999, 0, -16.7232, 3.0033998, 0, -16.7232, 3.0033998, 2.9166, -17.1112, 3.3913999, 3.3044999, -17.1112, 3.3913999, 0, -14.4892, 3.0033998, 4.2778, -15.582299, 3.3913999, 4.0366, -15.372, 3.0033998, 3.6132998, -14.4892, 3.0033998, 4.2778, -14.877199, 3.3913999, 4.6657, -15.582299, 3.3913999, 4.0366, -2.9566998, 3.3913999, 3.3446, -3.3446, 3.0033998, 0, -2.9566998, 3.3913999, 0, -2.9566998, 3.3913999, 3.3446, -3.3446, 3.0033998, 3.3446, -3.3446, 3.0033998, 0, -2.1794999, 2.8042998, 8.009399, -2.1397, 3.0033998, 5.1795, -1.7832999, 3.3913999, 5.2162, -2.1794999, 2.8042998, 8.009399, -2.5674999, 2.4164, 7.6215, -2.1397, 3.0033998, 5.1795, -6.6893, 3.3913999, 10.033899, -2.1794999, 2.8042998, 8.009399, -3.3446, 3.3913999, 10.033899, -6.6893, 3.3913999, 10.033899, -6.6893, 3.3913999, 7.0772, -2.1794999, 2.8042998, 8.009399, -1.7832999, 3.3913999, 5.2162, 0, 3.3913999, 6.6893, -2.1794999, 2.8042998, 8.009399, 0, 3.3913999, 6.6893, -1.7832999, 3.3913999, 5.2162, -2.9566998, 3.3913999, 3.3446, 0, 3.3913999, 6.6893, -2.9566998, 3.3913999, 3.3446, 0, 3.3913999, 3.3446, -3.3446, 3.3913999, 10.033899, 0, 3.3913999, 6.6893, 0, 3.3913999, 10.033899, -3.3446, 3.3913999, 10.033899, -2.1794999, 2.8042998, 8.009399, 0, 3.3913999, 6.6893, -2.1397, 3.0033998, 5.1795, -2.9566998, 3.3913999, 3.3446, -1.7832999, 3.3913999, 5.2162, -2.1397, 3.0033998, 5.1795, -3.3446, 3.0033998, 3.3446, -2.9566998, 3.3913999, 3.3446, -14.0948, 3.3913999, 5.5073, -14.4892, 3.0033998, 4.2778, -13.6942, 3.0033998, 5.1798, -14.0948, 3.3913999, 5.5073, -14.877199, 3.3913999, 4.6657, -14.4892, 3.0033998, 4.2778, -15.582299, 3.3913999, 4.0366, -16.7232, 3.0033998, 2.9166, -15.372, 3.0033998, 3.6132998, -15.582299, 3.3913999, 4.0366, -17.1112, 3.3913999, 3.3044999, -16.7232, 3.0033998, 2.9166, 20.198, 3.1490998, -10.033899, 19.798899, 3.3913999, -13.1096, 20.1539, 3.1299, -13.399799, 20.198, 3.1490998, -10.033899, 19.798899, 3.3913999, -10.033899, 19.798899, 3.3913999, -13.1096, 20.1539, 3.1299, -13.399799, 16.7232, 3.3913999, -13.1096, 16.8204, 3.138, -13.447599, 20.1539, 3.1299, -13.399799, 19.798899, 3.3913999, -13.1096, 16.7232, 3.3913999, -13.1096, 16.756899, 3.1650999, -16.7286, 12.738999, 3.3913999, -15.894899, 12.7471, 3.1766999, -16.1988, 16.756899, 3.1650999, -16.7286, 16.495699, 3.3913999, -16.459, 12.738999, 3.3913999, -15.894899, 16.8051, 3.1778, -13.479, 16.495699, 3.3913999, -16.459, 16.756899, 3.1650999, -16.7286, 16.8051, 3.1778, -13.479, 16.495699, 3.3913999, -13.42, 16.495699, 3.3913999, -16.459, 16.4543, 3.3913999, -13.3786, 16.8204, 3.138, -13.447599, 16.7232, 3.3913999, -13.1096, 16.8204, 3.138, -13.447599, 16.4543, 3.3913999, -13.3786, 16.495699, 3.3913999, -13.42, 16.8204, 3.138, -13.447599, 16.495699, 3.3913999, -13.42, 16.8051, 3.1778, -13.479, 10.033899, 3.0523999, -19.0294, 6.6893, 3.2782998, -18.727999, 6.6893, 3.0440998, -19.036999, 10.033899, 3.0523999, -19.0294, 10.033899, 3.2784998, -18.6997, 6.6893, 3.2782998, -18.727999, 12.723599, 2.9213, -19.2902, 10.033899, 3.2784998, -18.6997, 10.033899, 3.0523999, -19.0294, 12.723599, 2.9213, -19.2902, 12.446099, 3.1699998, -18.9401, 10.033899, 3.2784998, -18.6997, 12.446099, 3.1699998, -18.9401, 12.747, 3.1771, -16.1991, 12.460899, 3.3913999, -16.1674, 12.446099, 3.1699998, -18.9401, 12.723599, 2.9213, -19.2902, 12.747, 3.1771, -16.1991, 12.460899, 3.3913999, -16.1674, 10.033899, 3.2784998, -18.6997, 12.446099, 3.1699998, -18.9401, 10.033899, 3.3913999, -16.7232, 10.033899, 3.2784998, -18.6997, 12.460899, 3.3913999, -16.1674, 10.033899, 3.3913999, -16.7232, 12.460899, 3.3913999, -16.1674, 12.460899, 3.3913999, -16.1674, 6.6893, 3.3913999, -16.7232, 10.033899, 3.2784998, -18.6997, 10.033899, 3.3913999, -16.7232, 6.6893, 3.3913999, -16.7232, 6.6893, 3.2782998, -18.727999, 10.033899, 3.2784998, -18.6997, 12.738999, 3.3913999, -15.894899, 12.747, 3.1771, -16.1991, 12.7471, 3.1766999, -16.1988, 12.738999, 3.3913999, -15.894899, 12.460899, 3.3913999, -16.1674, 12.747, 3.1771, -16.1991, 12.460899, 3.3913999, -16.1674, 12.738999, 3.3913999, -15.894899, 12.460899, 3.3913999, -16.1674, 3.8388, 3.3913999, -16.7232, 4.4783998, 3.1067, -18.2665, 4.6443, 3.2897, -17.9799, 3.8388, 3.3913999, -16.7232, 3.5895, 3.2033, -16.7232, 4.4783998, 3.1067, -18.2665, 4.6821, 3.3913999, -13.3786, 3.5895, 3.2033, -16.7232, 3.8388, 3.3913999, -16.7232, 4.6821, 3.3913999, -13.3786, 4.4131, 3.2091, -13.357699, 3.5895, 3.2033, -16.7232, 6.6640997, 3.0702, -19.018099, 4.6443, 3.2897, -17.9799, 4.4783998, 3.1067, -18.2665, 6.6640997, 3.0702, -19.018099, 6.6705, 3.2772, -18.741, 4.6443, 3.2897, -17.9799, 4.6443, 3.2897, -17.9799, 6.6893, 3.3913999, -16.7232, 3.8388, 3.3913999, -16.7232, 6.6893, 3.3913999, -16.7232, 4.6443, 3.2897, -17.9799, 6.6705, 3.2772, -18.741, 6.6893, 3.3913999, -16.7232, 6.6705, 3.2772, -18.741, 6.6893, 3.2782998, -18.727999, 6.6893, 3.2782998, -18.727999, 6.6640997, 3.0702, -19.018099, 6.6893, 3.0440998, -19.036999, 6.6893, 3.2782998, -18.727999, 6.6705, 3.2772, -18.741, 6.6640997, 3.0702, -19.018099, 3.7529, 3.1712, -9.0494995, 0, 3.3913999, -8.7143, 0, 3.179, -9.0652, 3.7529, 3.1712, -9.0494995, 4.0225, 3.3913999, -8.7143, 0, 3.3913999, -8.7143, 4.0225, 3.3913999, -8.7143, 4.4131, 3.2091, -13.357699, 4.6821, 3.3913999, -13.3786, 4.0225, 3.3913999, -8.7143, 3.7529, 3.1712, -9.0494995, 4.4131, 3.2091, -13.357699, -3.6538, 3.1585999, -12.813399, -6.6893, 3.3913999, -12.538199, -6.6893, 3.175, -12.867, -3.6538, 3.1585999, -12.813399, -3.9101999, 3.3913999, -12.538199, -6.6893, 3.3913999, -12.538199, -3.6076, 3.1676, -10.0696, -3.9101999, 3.3913999, -12.538199, -3.6538, 3.1585999, -12.813399, -3.6076, 3.1676, -10.0696, -3.9101999, 3.3913999, -9.8592, -3.9101999, 3.3913999, -12.538199, -0.0081, 3.1864998, -9.063499, -3.9101999, 3.3913999, -9.8592, -3.6076, 3.1676, -10.0696, -0.0081, 3.1864998, -9.063499, 0, 3.3913999, -8.7143, -3.9101999, 3.3913999, -9.8592, 0, 3.3913999, -8.7143, -0.0081, 3.1864998, -9.063499, 0, 3.179, -9.0652, 0, 3.3913999, -8.7143, 0, 3.3913999, -8.7143, -0.0081, 3.1864998, -9.063499, -10.4043, 3.1608999, -13.4295, -10.699599, 3.3913999, -15.835, -10.4319, 3.1557999, -16.1556, -10.4043, 3.1608999, -13.4295, -10.699599, 3.3913999, -13.175799, -10.699599, 3.3913999, -15.835, -10.4319, 3.1557999, -16.1556, -13.3786, 3.3913999, -15.4154, -13.3786, 3.1771998, -15.7625, -10.4319, 3.1557999, -16.1556, -10.699599, 3.3913999, -15.835, -13.3786, 3.3913999, -15.4154, -10.699599, 3.3913999, -13.175799, -6.6893, 3.175, -12.867, -6.6893, 3.3913999, -12.538199, -10.699599, 3.3913999, -13.175799, -10.4043, 3.1608999, -13.4295, -6.6893, 3.175, -12.867, -16.130499, 3.6227, -13.623799, -16.0243, 3.3509998, -15.752, -15.7941, 3.5591, -15.4641, -16.130499, 3.6227, -13.623799, -16.3753, 3.4225, -13.6277, -16.0243, 3.3509998, -15.752, -13.3911, 3.1892, -15.757, -15.7941, 3.5591, -15.4641, -16.0243, 3.3509998, -15.752, -13.3911, 3.1892, -15.757, -13.396, 3.3925, -15.4333, -15.7941, 3.5591, -15.4641, -15.7941, 3.5591, -15.4641, -13.3786, 3.3913999, -13.3786, -16.130499, 3.6227, -13.623799, -13.3786, 3.3913999, -13.3786, -15.7941, 3.5591, -15.4641, -13.396, 3.3925, -15.4333, -13.3786, 3.3913999, -13.3786, -13.396, 3.3925, -15.4333, -13.3786, 3.3913999, -15.4154, -13.3786, 3.3913999, -15.4154, -13.3911, 3.1892, -15.757, -13.3786, 3.1771998, -15.7625, -13.3786, 3.3913999, -15.4154, -13.396, 3.3925, -15.4333, -13.3911, 3.1892, -15.757, -16.3753, 3.4225, -13.6277, -16.1241, 3.3832998, -9.5097, -16.4131, 3.1706, -9.788899, -16.3753, 3.4225, -13.6277, -16.130499, 3.6227, -13.623799, -16.1241, 3.3832998, -9.5097, -16.7232, 3.3913999, -6.6893, -13.3786, 3.3913999, -10.033899, -13.3786, 3.3913999, -6.6893, -16.7232, 3.3913999, -6.6893, -16.1241, 3.3832998, -9.5097, -13.3786, 3.3913999, -10.033899, -16.1241, 3.3832998, -9.5097, -13.3786, 3.3913999, -13.3786, -13.3786, 3.3913999, -10.033899, -16.1241, 3.3832998, -9.5097, -16.130499, 3.6227, -13.623799, -13.3786, 3.3913999, -13.3786, -16.130499, 3.6227, -13.623799, -16.1241, 3.3832998, -9.5097, -16.130499, 3.6227, -13.623799, -16.130499, 3.6227, -13.623799, -16.3753, 3.4225, -13.6277, -16.3753, 3.4225, -13.6277, -16.130499, 3.6227, -13.623799, -16.130499, 3.6227, -13.623799, -16.3753, 3.4225, -13.6277, -19.7545, 3.3913999, -2.7851999, -19.515, 3.2122, -6.6893, -19.280699, 3.3913999, -6.6893, -19.7545, 3.3913999, -2.7851999, -19.9489, 3.2063, -2.8082, -19.515, 3.2122, -6.6893, -19.280699, 3.3913999, -6.6893, -19.6161, 3.2075999, -9.7116995, -19.3965, 3.3909, -9.5092, -19.280699, 3.3913999, -6.6893, -19.515, 3.2122, -6.6893, -19.6161, 3.2075999, -9.7116995, -19.280699, 3.3913999, -6.6893, -16.1241, 3.3832998, -9.5097, -16.7232, 3.3913999, -6.6893, -19.280699, 3.3913999, -6.6893, -19.3965, 3.3909, -9.5092, -16.1241, 3.3832998, -9.5097, -16.1241, 3.3832998, -9.5097, -19.6237, 3.1655998, -9.757899, -16.4131, 3.1706, -9.788899, -19.6237, 3.1655998, -9.757899, -16.1241, 3.3832998, -9.5097, -19.3965, 3.3909, -9.5092, -19.6237, 3.1655998, -9.757899, -19.3965, 3.3909, -9.5092, -19.6161, 3.2075999, -9.7116995, -22.8657, 3.4937, 3.3613, -23.3676, 3.4459999, 0.1372, -23.170399, 3.6251, 0.122999996, -22.8657, 3.4937, 3.3613, -23.0809, 3.3083, 3.3615, -23.3676, 3.4459999, 0.1372, -23.170399, 3.6251, 0.122999996, -23.390299, 3.3349998, -2.6938999, -23.192099, 3.5300999, -2.4668999, -23.170399, 3.6251, 0.122999996, -23.3676, 3.4459999, 0.1372, -23.390299, 3.3349998, -2.6938999, -23.192099, 3.5300999, -2.4668999, -19.9489, 3.2063, -2.8082, -19.9511, 3.3913999, -2.5834, -23.192099, 3.5300999, -2.4668999, -23.390299, 3.3349998, -2.6938999, -19.9489, 3.2063, -2.8082, -23.192099, 3.5300999, -2.4668999, -20.0679, 3.3913999, 0, -23.170399, 3.6251, 0.122999996, -20.0679, 3.3913999, 0, -23.192099, 3.5300999, -2.4668999, -19.9511, 3.3913999, -2.5834, -19.9511, 3.3913999, -2.5834, -23.192099, 3.5300999, -2.4668999, -19.9511, 3.3913999, -2.5834, -22.8657, 3.4937, 3.3613, -20.0679, 3.3913999, 0, -20.0679, 3.3913999, 3.3446, -22.8657, 3.4937, 3.3613, -23.170399, 3.6251, 0.122999996, -20.0679, 3.3913999, 0, -19.7545, 3.3913999, -2.7851999, -19.9489, 3.2063, -2.8082, -19.9489, 3.2063, -2.8082, -19.7545, 3.3913999, -2.7851999, -19.9511, 3.3913999, -2.5834, -19.9489, 3.2063, -2.8082, -19.9511, 3.3913999, -2.5834, -19.7545, 3.3913999, -2.7851999, -19.9511, 3.3913999, -2.5834, -23.0777, 3.3197, 3.3734, -22.3151, 3.3913999, 6.6893, -22.563799, 3.2219, 6.6893, -23.0777, 3.3197, 3.3734, -22.8764, 3.4937, 3.3741, -22.3151, 3.3913999, 6.6893, -22.8764, 3.4937, 3.3741, -20.0679, 3.3913999, 6.6893, -22.3151, 3.3913999, 6.6893, -20.0679, 3.3913999, 6.6893, -22.8764, 3.4937, 3.3741, -22.8657, 3.4937, 3.3613, -20.0679, 3.3913999, 6.6893, -22.8657, 3.4937, 3.3613, -20.0679, 3.3913999, 3.3446, -22.8657, 3.4937, 3.3613, -23.0777, 3.3197, 3.3734, -23.0809, 3.3083, 3.3615, -22.8657, 3.4937, 3.3613, -22.8764, 3.4937, 3.3741, -23.0777, 3.3197, 3.3734, -16.285099, 3.3913999, 9.5865, -20.0679, 3.1943, 9.407499, -20.0679, 3.3913999, 9.167399, -16.285099, 3.3913999, 9.5865, -16.2818, 3.1913998, 9.8056, -20.0679, 3.1943, 9.407499, -22.791399, 3.2124999, 9.4432, -20.0679, 3.3913999, 9.167399, -20.0679, 3.1943, 9.407499, -22.791399, 3.2124999, 9.4432, -22.5541, 3.3913999, 9.2058, -20.0679, 3.3913999, 9.167399, -22.5541, 3.3913999, 9.2058, -22.563799, 3.2219, 6.6893, -22.3151, 3.3913999, 6.6893, -22.5541, 3.3913999, 9.2058, -22.791399, 3.2124999, 9.4432, -22.563799, 3.2219, 6.6893, -22.791399, 3.2124999, 9.4432, -22.5541, 3.3913999, 9.2058, -22.791399, 3.2124999, 9.4432, -13.3786, 3.3922, 12.571899, -16.076399, 3.04, 12.735, -15.847199, 3.2614, 12.4973, -13.3786, 3.3922, 12.571899, -13.3786, 3.194, 12.8089, -16.076399, 3.04, 12.735, -15.847199, 3.2614, 12.4973, -16.2818, 3.1913998, 9.8056, -16.053999, 3.3913999, 9.8082, -15.847199, 3.2614, 12.4973, -16.076399, 3.04, 12.735, -16.2818, 3.1913998, 9.8056, -15.847199, 3.2614, 12.4973, -13.3786, 3.3913999, 10.033899, -13.3786, 3.3922, 12.571899, -15.847199, 3.2614, 12.4973, -16.053999, 3.3913999, 9.8082, -13.3786, 3.3913999, 10.033899, -16.053999, 3.3913999, 9.8082, -15.847199, 3.2614, 12.4973, -16.053999, 3.3913999, 9.8082, -16.285099, 3.3913999, 9.5865, -16.2818, 3.1913998, 9.8056, -16.2818, 3.1913998, 9.8056, -16.285099, 3.3913999, 9.5865, -16.053999, 3.3913999, 9.8082, -16.2818, 3.1913998, 9.8056, -16.053999, 3.3913999, 9.8082, -16.285099, 3.3913999, 9.5865, -16.053999, 3.3913999, 9.8082, -5.9742, 3.3994, 12.8817, -10.033899, 2.9384, 12.4537, -10.033899, 3.2142, 12.1061, -5.9742, 3.3994, 12.8817, -6.2748, 3.126, 13.122, -10.033899, 2.9384, 12.4537, -10.033899, 3.2142, 12.1061, -13.3786, 3.194, 12.8089, -13.3786, 3.3922, 12.571899, -10.033899, 3.2142, 12.1061, -10.033899, 2.9384, 12.4537, -13.3786, 3.194, 12.8089, -10.033899, 3.2142, 12.1061, -6.6893, 3.3913999, 10.033899, -5.9742, 3.3994, 12.8817, -10.033899, 3.2142, 12.1061, -10.033899, 3.3913999, 10.033899, -6.6893, 3.3913999, 10.033899, -13.3786, 3.3922, 12.571899, -10.033899, 3.3913999, 10.033899, -10.033899, 3.2142, 12.1061, -13.3786, 3.3922, 12.571899, -13.3786, 3.3913999, 10.033899, -10.033899, 3.3913999, 10.033899, -13.3786, 3.3913999, 10.033899, -13.3786, 3.3922, 12.571899, -13.3786, 3.3922, 12.571899, -5.9742, 3.3994, 12.8817, -3.3446, 3.3913999, 10.033899, -3.3446, 3.3913999, 13.3786, -5.9742, 3.3994, 12.8817, -6.6893, 3.3913999, 10.033899, -3.3446, 3.3913999, 10.033899, -13.3786, 3.3922, 12.571899, -13.3786, 3.194, 12.8089, -13.3786, 3.194, 12.8089, -13.3786, 3.3922, 12.571899, -13.3786, 3.3922, 12.571899, -13.3786, 3.194, 12.8089, 3.6399, 3.3913999, 16.2405, 0, 2.9636998, 16.0179, 0, 3.2243998, 15.7093, 3.6399, 3.3913999, 16.2405, 3.6425998, 3.1413999, 16.5127, 0, 2.9636998, 16.0179, 0, 3.2243998, 15.7093, -3.3446, 3.1497998, 15.914599, -3.3446, 3.3913999, 15.6011, 0, 3.2243998, 15.7093, 0, 2.9636998, 16.0179, -3.3446, 3.1497998, 15.914599, -6.2558, 3.1427999, 16.4098, -3.3446, 3.3913999, 15.6011, -3.3446, 3.1497998, 15.914599, -6.2558, 3.1427999, 16.4098, -5.9737997, 3.392, 16.087599, -3.3446, 3.3913999, 15.6011, -3.3446, 3.3913999, 15.6011, 0, 3.3913999, 13.3786, 0, 3.2243998, 15.7093, -3.3446, 3.3913999, 15.6011, -3.3446, 3.3913999, 13.3786, 0, 3.3913999, 13.3786, 0, 3.2243998, 15.7093, 3.3446, 3.3913999, 13.3786, 3.6399, 3.3913999, 16.2405, 0, 3.2243998, 15.7093, 0, 3.3913999, 13.3786, 3.3446, 3.3913999, 13.3786, -5.9737997, 3.392, 16.087599, -3.3446, 3.3913999, 13.3786, -3.3446, 3.3913999, 15.6011, -5.9737997, 3.392, 16.087599, -5.9742, 3.3994, 12.8817, -3.3446, 3.3913999, 13.3786, -6.2748, 3.1260998, 16.4145, -5.9737997, 3.392, 16.087599, -6.2558, 3.1427999, 16.4098, -5.9737997, 3.392, 16.087599, -6.2748, 3.1260998, 16.4145, -6.2748, 3.126, 13.122, -5.9737997, 3.392, 16.087599, -6.2748, 3.126, 13.122, -5.9742, 3.3994, 12.8817, 6.6893, 3.3913999, 18.9861, 3.9266999, 3.5967, 19.6223, 4.1273, 3.7335, 19.3804, 6.6893, 3.3913999, 18.9861, 6.6893, 3.2050998, 19.228699, 3.9266999, 3.5967, 19.6223, 4.1273, 3.7335, 19.3804, 3.6536, 3.2078998, 16.5669, 3.8651, 3.3981998, 16.5647, 4.1273, 3.7335, 19.3804, 3.9266999, 3.5967, 19.6223, 3.6536, 3.2078998, 16.5669, 3.8651, 3.3981998, 16.5647, 6.6893, 3.3913999, 18.9861, 4.1273, 3.7335, 19.3804, 6.6893, 3.3913999, 18.9861, 3.8651, 3.3981998, 16.5647, 3.9148, 3.3913999, 16.514599, 6.6893, 3.3913999, 18.9861, 3.9148, 3.3913999, 16.514599, 6.6893, 3.3913999, 16.7232, 3.9148, 3.3913999, 16.514599, 3.6425998, 3.1413999, 16.5127, 3.6399, 3.3913999, 16.2405, 3.6425998, 3.1413999, 16.5127, 3.9148, 3.3913999, 16.514599, 3.8651, 3.3981998, 16.5647, 3.6425998, 3.1413999, 16.5127, 3.8651, 3.3981998, 16.5647, 3.6536, 3.2078998, 16.5669, 9.3066, 3.2186, 19.0653, 10.3743, 3.3913999, 16.7232, 10.5913, 3.1985998, 16.7232, 9.3066, 3.2186, 19.0653, 9.1275, 3.3913999, 18.830599, 10.3743, 3.3913999, 16.7232, 9.1275, 3.3913999, 18.830599, 6.6991997, 3.2148, 19.2255, 6.6949, 3.3913999, 18.9914, 9.1275, 3.3913999, 18.830599, 9.3066, 3.2186, 19.0653, 6.6991997, 3.2148, 19.2255, 6.6893, 3.3913999, 18.9861, 6.6991997, 3.2148, 19.2255, 6.6893, 3.2050998, 19.228699, 6.6893, 3.3913999, 18.9861, 6.6949, 3.3913999, 18.9914, 6.6991997, 3.2148, 19.2255, 9.8131, 3.3546, 13.1979, 10.5893, 3.2117, 16.7106, 10.403999, 3.3911998, 16.6938, 9.8131, 3.3546, 13.1979, 10.0546, 3.1885, 13.4071, 10.5893, 3.2117, 16.7106, 6.6893, 3.3913999, 13.3786, 10.3743, 3.3913999, 16.7232, 6.6893, 3.3913999, 16.7232, 10.3743, 3.3913999, 16.7232, 6.6893, 3.3913999, 13.3786, 9.8131, 3.3546, 13.1979, 10.3743, 3.3913999, 16.7232, 9.8131, 3.3546, 13.1979, 10.403999, 3.3911998, 16.6938, 6.6893, 3.3913999, 13.3786, 10.033899, 3.3913999, 10.033899, 9.8131, 3.3546, 13.1979, 6.6893, 3.3913999, 13.3786, 6.6893, 3.3913999, 10.033899, 10.033899, 3.3913999, 10.033899, 10.3743, 3.3913999, 16.7232, 10.5893, 3.2117, 16.7106, 10.5913, 3.1985998, 16.7232, 10.3743, 3.3913999, 16.7232, 10.403999, 3.3911998, 16.6938, 10.5893, 3.2117, 16.7106, 13.1839, 3.2744, 6.6893, 13.5511, 3.1797, 6.6893, 13.4036, 3.0749998, 6.6893, 13.1839, 3.2744, 6.6893, 13.3786, 3.2744, 6.496, 13.5511, 3.1797, 6.6893, 12.9758, 3.2036, 10.033899, 13.1839, 3.2744, 6.6893, 13.4036, 3.0749998, 6.6893, 12.9758, 3.2036, 10.033899, 12.7325, 3.3913999, 10.033899, 13.1839, 3.2744, 6.6893, 13.2631, 3.5611, 13.4043, 12.7325, 3.3913999, 10.033899, 12.9758, 3.2036, 10.033899, 13.2631, 3.5611, 13.4043, 13.025499, 3.6999, 13.1985, 12.7325, 3.3913999, 10.033899, 10.033899, 3.3913999, 10.033899, 13.1839, 3.2744, 6.6893, 12.7325, 3.3913999, 10.033899, 10.033899, 3.3913999, 10.033899, 10.033899, 3.3913999, 6.6893, 13.1839, 3.2744, 6.6893, 9.8131, 3.3546, 13.1979, 12.7325, 3.3913999, 10.033899, 13.025499, 3.6999, 13.1985, 9.8131, 3.3546, 13.1979, 10.033899, 3.3913999, 10.033899, 12.7325, 3.3913999, 10.033899, 10.033899, 3.3913999, 3.3446, 13.1839, 3.2744, 6.6893, 10.033899, 3.3913999, 6.6893, 13.1839, 3.2744, 6.6893, 10.033899, 3.3913999, 3.3446, 13.3786, 3.3913999, 3.3446, 13.1839, 3.2744, 6.6893, 13.3786, 3.3913999, 3.3446, 13.3786, 3.2744, 6.496, 13.025499, 3.6999, 13.1985, 10.0546, 3.1885, 13.4071, 9.8131, 3.3546, 13.1979, 13.025499, 3.6999, 13.1985, 13.2631, 3.5611, 13.4043, 10.0546, 3.1885, 13.4071, 13.2631, 3.5611, 13.4043, 13.2631, 3.5611, 13.4043, 13.025499, 3.6999, 13.1985, 14.0489, 3.0042, -3.3446, 13.1206, 3.3028998, -3.3446, 13.3786, 3.3028998, -3.6027, 14.0489, 3.0042, -3.3446, 13.3786, 3.3028998, -3.0865998, 13.1206, 3.3028998, -3.3446, 16.7232, 3.1513, -3.602, 14.0489, 3.0042, -3.3446, 13.3786, 3.3028998, -3.6027, 16.7232, 3.1513, -3.602, 16.7127, 2.8734, -3.3446, 14.0489, 3.0042, -3.3446, 13.3786, 3.3028998, -3.6027, 16.7232, 3.3913999, -6.6893, 16.7232, 3.1513, -3.602, 13.3786, 3.3028998, -3.6027, 13.3786, 3.3913999, -6.6893, 16.7232, 3.3913999, -6.6893, 10.033899, 3.3913999, -6.6893, 13.1206, 3.3028998, -3.3446, 10.033899, 3.3913999, -3.3446, 13.1206, 3.3028998, -3.3446, 10.033899, 3.3913999, -6.6893, 13.3786, 3.3913999, -6.6893, 13.1206, 3.3028998, -3.3446, 13.3786, 3.3913999, -6.6893, 13.3786, 3.3028998, -3.6027, 20.0679, 0, 0, 14.0489, 3.0042, -3.3446, 20.961199, 0, -3.3446, 20.0679, 0, 3.3446, 14.0489, 3.0042, -3.3446, 20.0679, 0, 0, 14.0489, 3.0042, -3.3446, 13.3786, 3.3913999, 0, 13.3786, 3.3028998, -3.0865998, 14.0489, 3.0042, -3.3446, 13.3786, 3.2744, 6.496, 13.3786, 3.3913999, 0, 14.0489, 3.0042, -3.3446, 20.0679, 0, 3.3446, 13.3786, 3.2744, 6.496, 13.3786, 3.3913999, 0, 13.3786, 3.2744, 6.496, 13.3786, 3.3913999, 3.3446, 13.3786, 3.2744, 6.496, 20.0679, 0, 3.3446, 16.7232, 1.5715, 6.6893, 16.7232, 1.5715, 6.6893, 20.0679, 0, 3.3446, 20.0679, -0.124199994, 6.6893, 13.3786, 3.2744, 6.496, 16.7232, 1.5715, 6.6893, 13.5511, 3.1797, 6.6893, 10.033899, 3.3913999, -3.3446, 13.3786, 3.3913999, 0, 10.033899, 3.3913999, 0, 13.3786, 3.3913999, 0, 10.033899, 3.3913999, -3.3446, 13.1206, 3.3028998, -3.3446, 13.3786, 3.3913999, 0, 13.1206, 3.3028998, -3.3446, 13.3786, 3.3028998, -3.0865998, 19.831999, 3.3902998, -10.0008, 20.147, 3.0588999, -6.6893, 19.8322, 3.2891998, -6.6893, 19.831999, 3.3902998, -10.0008, 20.1796, 3.182, -9.995999, 20.147, 3.0588999, -6.6893, 19.8322, 3.2891998, -6.6893, 20.1349, 2.7974, -3.3783998, 19.8322, 3.0491, -3.602, 19.8322, 3.2891998, -6.6893, 20.147, 3.0588999, -6.6893, 20.1349, 2.7974, -3.3783998, 16.7232, 3.1513, -3.602, 19.8322, 3.2891998, -6.6893, 19.8322, 3.0491, -3.602, 16.7232, 3.1513, -3.602, 16.7232, 3.3913999, -6.6893, 19.8322, 3.2891998, -6.6893, 16.7232, 3.3913999, -10.033899, 19.8322, 3.2891998, -6.6893, 16.7232, 3.3913999, -6.6893, 19.8322, 3.2891998, -6.6893, 16.7232, 3.3913999, -10.033899, 19.798899, 3.3913999, -10.033899, 19.8322, 3.2891998, -6.6893, 19.798899, 3.3913999, -10.033899, 19.831999, 3.3902998, -10.0008, 20.198, 3.1490998, -10.033899, 19.831999, 3.3902998, -10.0008, 19.798899, 3.3913999, -10.033899, 20.198, 3.1490998, -10.033899, 20.1796, 3.182, -9.995999, 19.831999, 3.3902998, -10.0008, 20.144999, 2.7605999, -3.3446, 19.8322, 3.0491, -3.602, 20.1349, 2.7974, -3.3783998, 19.8322, 3.0491, -3.602, 20.144999, 2.7605999, -3.3446, 16.7127, 2.8734, -3.3446, 19.8322, 3.0491, -3.602, 16.7127, 2.8734, -3.3446, 16.7232, 3.1513, -3.602, 15.0508995, 0, 6.6893, 16.7232, 1.5715, 6.6893, 16.7232, 0, 6.6893, 16.7232, 0, 6.6893, 16.7232, 1.5715, 6.6893, 20.0679, -0.124199994, 6.6893, 15.0508995, 0, 6.6893, 13.5511, 3.1797, 6.6893, 16.7232, 1.5715, 6.6893, 15.0508995, 0, 6.6893, 13.4036, 3.0749998, 6.6893, 13.5511, 3.1797, 6.6893, 13.4036, 3.0749998, 6.6893, 15.0508995, 0, 6.6893, 13.8046, 0, 6.6893, -16.7232, 0.6137, 0, -15.7546, 0.6137, 3.5372, -16.7232, 0.6137, 2.9166, -15.7546, 0.6137, 3.5372, -16.7232, 0.6137, 0, -13.3786, 0.6137, 0, -16.7232, 0.6137, 0, -15.8036995, 0.6137, -2.7091, -13.3786, 0.6137, -3.3446, -16.7232, 0.6137, 0, -13.3786, 0.6137, -3.3446, -13.3786, 0.6137, 0, -13.3786, 0.6137, 0, -13.3786, 0.6137, -3.3446, -10.033899, 0.6137, -3.3446, -15.7546, 0.6137, 3.5372, -13.3786, 0.6137, 0, -14.4892, 0.6137, 4.2778, -13.3786, 0.6137, 0, -10.033899, 0.6137, -3.3446, -10.033899, 0.6137, 0, -14.4892, 0.6137, 4.2778, -13.3786, 0.6137, 0, -10.033899, 0.6137, 0, -10.033899, 0.6137, 0, -10.033899, 0.6137, -3.3446, -6.6893, 0.6137, -3.3446, -10.033899, 0.6137, 0, -6.6893, 0.6137, -3.3446, -6.6893, 0.6137, 0, -6.6893, 0.6137, 0, -6.6893, 0.6137, -3.3446, -4.0257998, 0.6137, -1.6911999, -6.6893, 0.6137, 0, -4.0257998, 0.6137, -1.6911999, -3.3446, 0.6137, 0, -10.033899, 0.6137, 3.3446, -10.033899, 0.6137, 0, -6.6893, 0.6137, 0, -14.4892, 0.6137, 4.2778, -10.033899, 0.6137, 0, -10.033899, 0.6137, 3.3446, -6.6893, 0.6137, 3.3446, -6.6893, 0.6137, 0, -3.3446, 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.3446, 0.6137, 0, -3.3446, 0.6137, 3.3446, -10.033899, 0.6137, 6.6893, -14.4892, 0.6137, 4.2778, -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.677099, 0.6137, 5.2616, -14.4892, 0.6137, 4.2778, -13.677099, 0.6137, 5.2616, -10.033899, 0.6137, 6.6893, -12.747499, 0.6137, 6.3671, -6.6893, 0.6137, 3.3446, -2.5674999, 0.0266, 7.6215, -6.6893, 0.6137, 6.6893, -2.5674999, 0.0266, 7.6215, -6.6893, 0.6137, 3.3446, -3.3446, 0.6137, 3.3446, -2.5674999, 0.0266, 7.6215, -3.3446, 0.6137, 3.3446, -2.1193, 0.6137, 5.2736998, -16.7232, 3.0033998, 2.9166, -15.7546, 0.6137, 3.5372, -15.372, 3.0033998, 3.6132998, -16.7232, 3.0033998, 2.9166, -16.7232, 0.6137, 2.9166, -15.7546, 0.6137, 3.5372, -14.4892, 3.0033998, 4.2778, -13.677099, 0.6137, 5.2616, -13.6942, 3.0033998, 5.1798, -14.4892, 3.0033998, 4.2778, -14.4892, 0.6137, 4.2778, -13.677099, 0.6137, 5.2616, -15.8036995, 3.0033998, -2.7091, -16.7232, 0.6137, 0, -16.7232, 3.0033998, 0, -15.8036995, 3.0033998, -2.7091, -15.8036995, 0.6137, -2.7091, -16.7232, 0.6137, 0, -16.7232, 3.0033998, 0, -16.7232, 0.6137, 2.9166, -16.7232, 3.0033998, 2.9166, -16.7232, 3.0033998, 0, -16.7232, 0.6137, 0, -16.7232, 0.6137, 2.9166, -3.3446, 3.0033998, 0, -4.0257998, 0.6137, -1.6911999, -4.0257998, 3.0033998, -1.6911999, -3.3446, 3.0033998, 0, -3.3446, 0.6137, 0, -4.0257998, 0.6137, -1.6911999, -3.3446, 3.0033998, 3.3446, -3.3446, 0.6137, 0, -3.3446, 3.0033998, 0, -3.3446, 3.0033998, 3.3446, -3.3446, 0.6137, 3.3446, -3.3446, 0.6137, 0, -12.747499, 3.0033998, 6.3671, -10.033899, 0.6137, 6.6893, -10.033899, 3.0033998, 6.6893, -12.747499, 3.0033998, 6.3671, -12.747499, 0.6137, 6.3671, -10.033899, 0.6137, 6.6893, -10.033899, 3.0033998, -3.3446, -13.3786, 0.6137, -3.3446, -13.3786, 3.0033998, -3.3446, -10.033899, 3.0033998, -3.3446, -10.033899, 0.6137, -3.3446, -13.3786, 0.6137, -3.3446, -6.6893, 3.0033998, -3.3446, -10.033899, 0.6137, -3.3446, -10.033899, 3.0033998, -3.3446, -6.6893, 3.0033998, -3.3446, -6.6893, 0.6137, -3.3446, -10.033899, 0.6137, -3.3446, -13.3786, 3.0033998, -3.3446, -15.8036995, 0.6137, -2.7091, -15.8036995, 3.0033998, -2.7091, -13.3786, 3.0033998, -3.3446, -13.3786, 0.6137, -3.3446, -15.8036995, 0.6137, -2.7091, -4.0257998, 3.0033998, -1.6911999, -6.6893, 0.6137, -3.3446, -6.6893, 3.0033998, -3.3446, -4.0257998, 3.0033998, -1.6911999, -4.0257998, 0.6137, -1.6911999, -6.6893, 0.6137, -3.3446, -6.6893, 3.0033998, 6.6893, -2.5674999, 0.0266, 7.6215, -2.5674999, 2.4164, 7.6215, -6.6893, 3.0033998, 6.6893, -6.6893, 0.6137, 6.6893, -2.5674999, 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.5674999, 2.4164, 7.6215, -2.1193, 0.6137, 5.2736998, -2.1397, 3.0033998, 5.1795, -2.5674999, 2.4164, 7.6215, -2.5674999, 0.0266, 7.6215, -2.1193, 0.6137, 5.2736998, -2.1193, 0.6137, 5.2736998, -3.3446, 3.0033998, 3.3446, -2.1397, 3.0033998, 5.1795, -2.1193, 0.6137, 5.2736998, -3.3446, 0.6137, 3.3446, -3.3446, 3.0033998, 3.3446, -13.677099, 0.6137, 5.2616, -12.747499, 3.0033998, 6.3671, -13.6942, 3.0033998, 5.1798, -13.677099, 0.6137, 5.2616, -12.747499, 0.6137, 6.3671, -12.747499, 3.0033998, 6.3671, -15.7546, 0.6137, 3.5372, -14.4892, 3.0033998, 4.2778, -15.372, 3.0033998, 3.6132998, -15.7546, 0.6137, 3.5372, -14.4892, 0.6137, 4.2778, -14.4892, 3.0033998, 4.2778, 21.1831, 0, -13.6544, 16.8204, 3.138, -13.447599, 18.0241, 0, -14.3019, 21.1831, 0, -13.6544, 20.1539, 3.1299, -13.399799, 16.8204, 3.138, -13.447599, 21.8888, 0, -10.033899, 20.1539, 3.1299, -13.399799, 21.1831, 0, -13.6544, 21.8888, 0, -10.033899, 20.198, 3.1490998, -10.033899, 20.1539, 3.1299, -13.399799, 17.2285, 0, -16.8036, 12.7471, 3.1766999, -16.1988, 13.7014, 0, -17.416399, 17.2285, 0, -16.8036, 16.756899, 3.1650999, -16.7286, 12.7471, 3.1766999, -16.1988, 16.8204, 3.138, -13.447599, 17.2285, 0, -16.8036, 18.0241, 0, -14.3019, 17.2285, 0, -16.8036, 16.8204, 3.138, -13.447599, 16.8051, 3.1778, -13.479, 17.2285, 0, -16.8036, 16.8051, 3.1778, -13.479, 16.756899, 3.1650999, -16.7286, 12.7471, 3.1766999, -16.1988, 13.3786, 0, -20.5992, 13.7014, 0, -17.416399, 13.3786, 0, -20.5992, 12.7471, 3.1766999, -16.1988, 12.747, 3.1771, -16.1991, 13.3786, 0, -20.5992, 12.747, 3.1771, -16.1991, 12.723599, 2.9213, -19.2902, 13.3786, 0, -20.5992, 10.033899, 3.0523999, -19.0294, 10.033899, 0, -20.467, 13.3786, 0, -20.5992, 12.723599, 2.9213, -19.2902, 10.033899, 3.0523999, -19.0294, 10.033899, 0, -20.467, 6.6893, 3.0440998, -19.036999, 6.6893, 0, -20.1318, 10.033899, 0, -20.467, 10.033899, 3.0523999, -19.0294, 6.6893, 3.0440998, -19.036999, 3.0140998, 0, -16.7232, 4.4131, 3.2091, -13.357699, 3.3446, -0.3989, -12.9443, 3.0140998, 0, -16.7232, 3.5895, 3.2033, -16.7232, 4.4131, 3.2091, -13.357699, 6.6893, 3.0440998, -19.036999, 3.3446, 0, -20.5992, 6.6893, 0, -20.1318, 3.3446, 0, -20.5992, 6.6893, 3.0440998, -19.036999, 6.6640997, 3.0702, -19.018099, 3.3446, 0, -20.5992, 6.6640997, 3.0702, -19.018099, 4.4783998, 3.1067, -18.2665, 3.3446, 0, -20.5992, 3.5895, 3.2033, -16.7232, 3.0140998, 0, -16.7232, 3.3446, 0, -20.5992, 4.4783998, 3.1067, -18.2665, 3.5895, 3.2033, -16.7232, 2.8241, 0, -10.4594, 0, 3.179, -9.0652, 0, 0, -10.7654, 2.8241, 0, -10.4594, 3.7529, 3.1712, -9.0494995, 0, 3.179, -9.0652, 3.3446, -0.3989, -12.9443, 3.7529, 3.1712, -9.0494995, 2.8241, 0, -10.4594, 3.3446, -0.3989, -12.9443, 4.4131, 3.2091, -13.357699, 3.7529, 3.1712, -9.0494995, 4.4131, 3.2091, -13.357699, 3.3446, -0.3989, -12.9443, 4.4131, 3.2091, -13.357699, -2.6297, 0, -10.5745, -3.6538, 3.1585999, -12.813399, -3.3446, 0, -13.3786, -2.6297, 0, -10.5745, -3.6076, 3.1676, -10.0696, -3.6538, 3.1585999, -12.813399, -3.3446, 0, -13.3786, -6.6893, 3.175, -12.867, -6.6893, 0, -14.2637, -3.3446, 0, -13.3786, -3.6538, 3.1585999, -12.813399, -6.6893, 3.175, -12.867, 0, 3.179, -9.0652, -2.6297, 0, -10.5745, 0, 0, -10.7654, -2.6297, 0, -10.5745, 0, 3.179, -9.0652, -0.0081, 3.1864998, -9.063499, -2.6297, 0, -10.5745, -0.0081, 3.1864998, -9.063499, -3.6076, 3.1676, -10.0696, -6.6893, 0, -14.2637, -10.4043, 3.1608999, -13.4295, -9.6168995, 0, -14.127899, -6.6893, 0, -14.2637, -6.6893, 3.175, -12.867, -10.4043, 3.1608999, -13.4295, -6.6893, 3.175, -12.867, -6.6893, 0, -14.2637, -6.6893, 3.175, -12.867, -9.6168995, 0, -14.127899, -10.4319, 3.1557999, -16.1556, -10.033899, 0, -16.7232, -9.6168995, 0, -14.127899, -10.4043, 3.1608999, -13.4295, -10.4319, 3.1557999, -16.1556, -10.033899, 0, -16.7232, -13.3786, 3.1771998, -15.7625, -13.3786, 0, -17.3364, -10.033899, 0, -16.7232, -10.4319, 3.1557999, -16.1556, -13.3786, 3.1771998, -15.7625, -16.7232, 0, -16.7232, -16.3753, 3.4225, -13.6277, -16.7232, 0, -13.3786, -16.7232, 0, -16.7232, -16.0243, 3.3509998, -15.752, -16.3753, 3.4225, -13.6277, -13.3786, 3.1771998, -15.7625, -16.7232, 0, -16.7232, -13.3786, 0, -17.3364, -16.7232, 0, -16.7232, -13.3786, 3.1771998, -15.7625, -13.3911, 3.1892, -15.757, -16.7232, 0, -16.7232, -13.3911, 3.1892, -15.757, -16.0243, 3.3509998, -15.752, -17.275, 0, -10.5169, -19.6237, 3.1655998, -9.757899, -20.0679, 0, -10.033899, -17.275, 0, -10.5169, -16.4131, 3.1706, -9.788899, -19.6237, 3.1655998, -9.757899, -16.7232, 0, -13.3786, -16.4131, 3.1706, -9.788899, -17.275, 0, -10.5169, -16.7232, 0, -13.3786, -16.3753, 3.4225, -13.6277, -16.4131, 3.1706, -9.788899, -16.3753, 3.4225, -13.6277, -16.7232, 0, -13.3786, -16.3753, 3.4225, -13.6277, -20.3764, 0, -6.6893, -19.9489, 3.2063, -2.8082, -20.0679, -0.3484, -3.5669, -20.3764, 0, -6.6893, -19.515, 3.2122, -6.6893, -19.9489, 3.2063, -2.8082, -19.6237, 3.1655998, -9.757899, -20.3764, 0, -6.6893, -20.0679, 0, -10.033899, -20.3764, 0, -6.6893, -19.6237, 3.1655998, -9.757899, -19.6161, 3.2075999, -9.7116995, -20.3764, 0, -6.6893, -19.6161, 3.2075999, -9.7116995, -19.515, 3.2122, -6.6893, -23.412498, -0.2913, 0.26119998, -23.0809, 3.3083, 3.3615, -23.412498, 0, 3.3446, -23.412498, -0.2913, 0.26119998, -23.3676, 3.4459999, 0.1372, -23.0809, 3.3083, 3.3615, -23.412498, 0, -3.3446, -23.3676, 3.4459999, 0.1372, -23.412498, -0.2913, 0.26119998, -23.412498, 0, -3.3446, -23.390299, 3.3349998, -2.6938999, -23.3676, 3.4459999, 0.1372, -20.0679, -0.3484, -3.5669, -23.390299, 3.3349998, -2.6938999, -23.412498, 0, -3.3446, -20.0679, -0.3484, -3.5669, -19.9489, 3.2063, -2.8082, -23.390299, 3.3349998, -2.6938999, -19.9489, 3.2063, -2.8082, -20.0679, -0.3484, -3.5669, -19.9489, 3.2063, -2.8082, -23.0809, 3.3083, 3.3615, -23.8078, 0, 6.6893, -23.412498, 0, 3.3446, -23.8078, 0, 6.6893, -23.0809, 3.3083, 3.3615, -23.0777, 3.3197, 3.3734, -23.8078, 0, 6.6893, -23.0777, 3.3197, 3.3734, -22.563799, 3.2219, 6.6893, -23.8078, 0, 6.6893, -22.791399, 3.2124999, 9.4432, -23.412498, 0, 10.033899, -23.8078, 0, 6.6893, -22.563799, 3.2219, 6.6893, -22.791399, 3.2124999, 9.4432, -23.412498, 0, 10.033899, -20.0679, 3.1943, 9.407499, -20.0679, 0, 10.033899, -23.412498, 0, 10.033899, -22.791399, 3.2124999, 9.4432, -20.0679, 3.1943, 9.407499, -22.791399, 3.2124999, 9.4432, -23.412498, 0, 10.033899, -22.791399, 3.2124999, 9.4432, -20.0679, 0, 10.033899, -16.2818, 3.1913998, 9.8056, -16.7232, 0, 10.033899, -20.0679, 0, 10.033899, -20.0679, 3.1943, 9.407499, -16.2818, 3.1913998, 9.8056, -16.7232, 0, 13.3786, -13.3786, 3.194, 12.8089, -13.3786, 0, 13.3786, -16.7232, 0, 13.3786, -16.076399, 3.04, 12.735, -13.3786, 3.194, 12.8089, -16.7232, 0, 10.033899, -16.076399, 3.04, 12.735, -16.7232, 0, 13.3786, -16.7232, 0, 10.033899, -16.2818, 3.1913998, 9.8056, -16.076399, 3.04, 12.735, -16.2818, 3.1913998, 9.8056, -16.7232, 0, 10.033899, -16.2818, 3.1913998, 9.8056, -13.3786, 0, 13.3786, -10.033899, 2.9384, 12.4537, -10.033899, 0, 13.3786, -13.3786, 0, 13.3786, -13.3786, 3.194, 12.8089, -10.033899, 2.9384, 12.4537, -13.3786, 3.194, 12.8089, -13.3786, 0, 13.3786, -13.3786, 3.194, 12.8089, -10.033899, 0, 13.3786, -6.2748, 3.126, 13.122, -6.6893, 0, 13.3786, -10.033899, 0, 13.3786, -10.033899, 2.9384, 12.4537, -6.2748, 3.126, 13.122, -6.6893, 0, 13.3786, -6.2748, 3.1260998, 16.4145, -6.6893, 0, 16.7232, -6.6893, 0, 13.3786, -6.2748, 3.126, 13.122, -6.2748, 3.1260998, 16.4145, -3.3446, 0, 16.7232, 0, 2.9636998, 16.0179, 0, 0, 16.7232, -3.3446, 0, 16.7232, -3.3446, 3.1497998, 15.914599, 0, 2.9636998, 16.0179, 0, 0, 16.7232, 3.6425998, 3.1413999, 16.5127, 3.3446, 0, 16.7232, 0, 0, 16.7232, 0, 2.9636998, 16.0179, 3.6425998, 3.1413999, 16.5127, -6.2748, 3.1260998, 16.4145, -3.3446, 0, 16.7232, -6.6893, 0, 16.7232, -3.3446, 0, 16.7232, -6.2748, 3.1260998, 16.4145, -6.2558, 3.1427999, 16.4098, -3.3446, 0, 16.7232, -6.2558, 3.1427999, 16.4098, -3.3446, 3.1497998, 15.914599, 3.3446, 0, 20.0679, 6.6893, 3.2050998, 19.228699, 6.6893, 0, 20.0679, 3.3446, 0, 20.0679, 3.9266999, 3.5967, 19.6223, 6.6893, 3.2050998, 19.228699, 3.6425998, 3.1413999, 16.5127, 3.3446, 0, 20.0679, 3.3446, 0, 16.7232, 3.3446, 0, 20.0679, 3.6425998, 3.1413999, 16.5127, 3.6536, 3.2078998, 16.5669, 3.3446, 0, 20.0679, 3.6536, 3.2078998, 16.5669, 3.9266999, 3.5967, 19.6223, 6.6893, 3.2050998, 19.228699, 10.572599, 0, 20.0679, 6.6893, 0, 20.0679, 10.572599, 0, 20.0679, 6.6893, 3.2050998, 19.228699, 6.6991997, 3.2148, 19.2255, 10.572599, 0, 20.0679, 6.6991997, 3.2148, 19.2255, 9.3066, 3.2186, 19.0653, 10.572599, 0, 20.0679, 10.5913, 3.1985998, 16.7232, 10.572599, 0, 16.7232, 10.572599, 0, 20.0679, 9.3066, 3.2186, 19.0653, 10.5913, 3.1985998, 16.7232, 10.5913, 3.1985998, 16.7232, 10.572599, 0, 13.918799, 10.572599, 0, 16.7232, 10.572599, 0, 13.918799, 10.5913, 3.1985998, 16.7232, 10.5893, 3.2117, 16.7106, 10.572599, 0, 13.918799, 10.5893, 3.2117, 16.7106, 10.0546, 3.1885, 13.4071, 10.572599, 0, 13.918799, 13.2631, 3.5611, 13.4043, 13.8046, 0, 13.918799, 10.572599, 0, 13.918799, 10.0546, 3.1885, 13.4071, 13.2631, 3.5611, 13.4043, 13.8046, 0, 13.918799, 12.9758, 3.2036, 10.033899, 13.8046, 0, 10.033899, 13.8046, 0, 13.918799, 13.2631, 3.5611, 13.4043, 12.9758, 3.2036, 10.033899, 13.8046, 0, 13.918799, 13.2631, 3.5611, 13.4043, 13.2631, 3.5611, 13.4043, 13.8046, 0, 10.033899, 13.4036, 3.0749998, 6.6893, 13.8046, 0, 6.6893, 13.8046, 0, 10.033899, 12.9758, 3.2036, 10.033899, 13.4036, 3.0749998, 6.6893, 20.961199, 0, -3.3446, 16.7127, 2.8734, -3.3446, 20.144999, 2.7605999, -3.3446, 20.961199, 0, -3.3446, 14.0489, 3.0042, -3.3446, 16.7127, 2.8734, -3.3446, 20.144999, 2.7605999, -3.3446, 21.2349, 0, -6.6893, 20.961199, 0, -3.3446, 21.2349, 0, -6.6893, 20.144999, 2.7605999, -3.3446, 20.1349, 2.7974, -3.3783998, 21.2349, 0, -6.6893, 20.1349, 2.7974, -3.3783998, 20.147, 3.0588999, -6.6893, 20.147, 3.0588999, -6.6893, 21.8888, 0, -10.033899, 21.2349, 0, -6.6893, 21.8888, 0, -10.033899, 20.147, 3.0588999, -6.6893, 20.1796, 3.182, -9.995999, 21.8888, 0, -10.033899, 20.1796, 3.182, -9.995999, 20.198, 3.1490998, -10.033899, 23.412498, 0, -26.7572, 26.7572, -0.2593, -23.412498, 23.412498, 0, -23.412498, 26.7572, -0.2593, -23.412498, 23.412498, 0, -26.7572, 26.7572, -0.2593, -25.084799, 26.7572, -0.2593, -25.084799, 23.412498, 0, -26.7572, 26.7572, -0.2593, -26.7572, 26.7572, -0.2593, -21.7402, 23.412498, 0, -23.412498, 26.7572, -0.2593, -23.412498, 26.7572, -0.2593, -20.0679, 23.412498, 0, -23.412498, 26.7572, -0.2593, -21.7402, 23.412498, 0, -23.412498, 26.7572, -0.2593, -20.0679, 23.412498, 0, -20.0679, 26.7572, -0.2593, -18.3955, 23.412498, 0, -20.0679, 26.7572, -0.2593, -20.0679, 26.7572, -0.2593, -16.7232, 23.412498, 0, -20.0679, 26.7572, -0.2593, -18.3955, 23.412498, 0, -20.0679, 26.7572, -0.2593, -16.7232, 23.412498, 0, -16.7232, 26.7572, -0.2593, -15.0508995, 23.412498, 0, -16.7232, 26.7572, -0.2593, -16.7232, 26.7572, -0.2593, -13.3786, 23.412498, 0, -16.7232, 26.7572, -0.2593, -15.0508995, 23.412498, 0, -16.7232, 26.7572, -0.2593, -13.3786, 23.412498, 0, -13.3786, 26.7572, -0.2593, -11.7063, 23.412498, 0, -13.3786, 26.7572, -0.2593, -13.3786, 26.7572, -0.2593, -10.033899, 23.412498, 0, -13.3786, 26.7572, -0.2593, -11.7063, 23.412498, 0, -13.3786, 26.7572, -0.2593, -10.033899, 23.412498, 0, -10.033899, 26.7572, -0.2593, -8.3616, 23.412498, 0, -10.033899, 26.7572, -0.2593, -10.033899, 26.7572, -0.2593, -6.6893, 23.412498, 0, -10.033899, 26.7572, -0.2593, -8.3616, 23.412498, 0, -10.033899, 26.7572, -0.2593, -6.6893, 23.412498, 0, -6.6893, 26.7572, -0.2593, -5.0169997, 23.412498, 0, -6.6893, 26.7572, -0.2593, -6.6893, 26.7572, -0.2593, -3.3446, 23.412498, 0, -6.6893, 26.7572, -0.2593, -5.0169997, 23.412498, 0, -6.6893, 26.7572, -0.2593, -3.3446, 23.412498, 0, -3.3446, 26.7572, -0.2593, -1.6723, 23.412498, 0, -3.3446, 26.7572, -0.2593, -3.3446, 26.7572, -0.2593, 0, 23.412498, 0, -3.3446, 26.7572, -0.2593, -1.6723, 23.412498, 0, -3.3446, 26.7572, -0.2593, 0, 23.412498, 0, 0, 26.7572, -0.2593, 1.6723, 23.412498, 0, 0, 26.7572, -0.2593, 0, 26.7572, -0.2593, 3.3446, 23.412498, 0, 0, 26.7572, -0.2593, 1.6723, 23.412498, 0, 0, 26.7572, -0.2593, 3.3446, 23.412498, 0, 3.3446, 26.7572, -0.2593, 5.0169997, 23.412498, 0, 3.3446, 26.7572, -0.2593, 3.3446, 26.7572, -0.2593, 6.6893, 23.412498, 0, 3.3446, 26.7572, -0.2593, 5.0169997, 23.412498, 0, 3.3446, 26.7572, -0.2593, 6.6893, 23.412498, 0, 6.6893, 26.7572, -0.2593, 8.3616, 23.412498, 0, 6.6893, 26.7572, -0.2593, 6.6893, 26.7572, -0.2593, 10.033899, 23.412498, 0, 6.6893, 26.7572, -0.2593, 8.3616, 23.412498, 0, 6.6893, 26.7572, -0.2593, 10.033899, 23.412498, 0, 10.033899, 26.7572, -0.2593, 11.7063, 23.412498, 0, 10.033899, 26.7572, -0.2593, 10.033899, 26.7572, -0.2593, 13.3786, 23.412498, 0, 10.033899, 26.7572, -0.2593, 11.7063, 23.412498, 0, 10.033899, 26.7572, -0.2593, 13.3786, 23.412498, 0, 13.3786, 26.7572, -0.2593, 15.0508995, 23.412498, 0, 13.3786, 26.7572, -0.2593, 13.3786, 26.7572, -0.2593, 16.7232, 23.412498, 0, 13.3786, 26.7572, -0.2593, 15.0508995, 23.412498, 0, 13.3786, 26.7572, -0.2593, 16.7232, 23.412498, 0, 16.7232, 26.7572, -0.2593, 18.3955, 23.412498, 0, 16.7232, 26.7572, -0.2593, 16.7232, 26.7572, -0.2593, 20.0679, 23.412498, 0, 16.7232, 26.7572, -0.2593, 18.3955, 23.412498, 0, 16.7232, 26.7572, -0.2593, 20.0679, 23.412498, 0, 20.0679, 26.7572, -0.2593, 21.7402, 23.412498, 0, 20.0679, 26.7572, -0.2593, 20.0679, 26.7572, -0.2593, 23.412498, 23.412498, 0, 20.0679, 26.7572, -0.2593, 21.7402, 23.412498, 0, 20.0679, 26.7572, -0.2593, 23.412498, 23.412498, 0, 23.412498, 26.7572, -0.2593, 25.084799, 23.412498, 0, 23.412498, 26.7572, -0.2593, 23.412498, 26.7572, -0.2593, 26.7572, 23.412498, 0, 23.412498, 26.7572, -0.2593, 25.084799, 23.412498, 0, 23.412498, 26.7572, -0.2593, 26.7572, 23.412498, 0, 26.7572, -3.3446, 0, -23.412498, 0, 0, -26.7572, 0, 0, -23.412498, 0, 0, -23.412498, 0, 0, -26.7572, 3.3446, 0, -26.7572, -3.3446, 0, -23.412498, -3.3446, 0, -26.7572, 0, 0, -26.7572, 0, 0, -23.412498, 3.3446, 0, -26.7572, 3.3446, 0, -23.412498, 3.3446, 0, -23.412498, 3.3446, 0, -26.7572, 6.6893, 0, -26.7572, -3.3446, 0, -20.0679, -3.3446, 0, -23.412498, 0, 0, -23.412498, 3.3446, 0, -23.412498, 6.6893, 0, -26.7572, 6.6893, 0, -23.412498, 6.6893, 0, -23.412498, 6.6893, 0, -26.7572, 10.033899, 0, -26.7572, 0, 0, -20.0679, 0, 0, -23.412498, 3.3446, 0, -23.412498, -3.3446, 0, -20.0679, 0, 0, -23.412498, 0, 0, -20.0679, 3.3446, 0, -20.5992, 3.3446, 0, -23.412498, 6.6893, 0, -23.412498, 0, 0, -20.0679, 3.3446, 0, -23.412498, 3.3446, 0, -20.5992, 3.3446, 0, -20.5992, 6.6893, 0, -23.412498, 6.6893, 0, -20.1318, 6.6893, 0, -23.412498, 10.033899, 0, -26.7572, 10.033899, 0, -23.412498, 6.6893, 0, -20.1318, 6.6893, 0, -23.412498, 10.033899, 0, -23.412498, 10.033899, 0, -23.412498, 10.033899, 0, -26.7572, 13.3786, 0, -26.7572, 6.6893, 0, -20.1318, 10.033899, 0, -23.412498, 10.033899, 0, -20.467, 10.033899, 0, -23.412498, 13.3786, 0, -26.7572, 13.3786, 0, -23.412498, 10.033899, 0, -20.467, 10.033899, 0, -23.412498, 13.3786, 0, -23.412498, 13.3786, 0, -23.412498, 13.3786, 0, -26.7572, 16.7232, 0, -26.7572, 10.033899, 0, -20.467, 13.3786, 0, -23.412498, 13.3786, 0, -20.5992, 13.3786, 0, -23.412498, 16.7232, 0, -26.7572, 16.7232, 0, -23.412498, 13.3786, 0, -20.5992, 13.3786, 0, -23.412498, 16.7232, 0, -23.412498, 16.7232, 0, -23.412498, 16.7232, 0, -26.7572, 20.0679, 0, -26.7572, 13.3786, 0, -20.5992, 16.7232, 0, -23.412498, 16.7232, 0, -20.0679, 13.7014, 0, -17.416399, 13.3786, 0, -20.5992, 16.7232, 0, -20.0679, 13.7014, 0, -17.416399, 16.7232, 0, -20.0679, 17.2285, 0, -16.8036, 16.7232, 0, -23.412498, 20.0679, 0, -26.7572, 20.0679, 0, -23.412498, 16.7232, 0, -20.0679, 16.7232, 0, -23.412498, 20.0679, 0, -23.412498, 20.0679, 0, -23.412498, 20.0679, 0, -26.7572, 23.412498, 0, -26.7572, 20.0679, 0, -23.412498, 23.412498, 0, -26.7572, 23.412498, 0, -23.412498, 16.7232, 0, -20.0679, 20.0679, 0, -23.412498, 20.0679, 0, -20.0679, 20.0679, 0, -20.0679, 20.0679, 0, -23.412498, 23.412498, 0, -23.412498, 17.2285, 0, -16.8036, 16.7232, 0, -20.0679, 20.0679, 0, -20.0679, 20.0679, 0, -20.0679, 23.412498, 0, -23.412498, 23.412498, 0, -20.0679, 17.2285, 0, -16.8036, 20.0679, 0, -20.0679, 20.0679, 0, -16.7232, 20.0679, 0, -16.7232, 20.0679, 0, -20.0679, 23.412498, 0, -20.0679, 18.0241, 0, -14.3019, 17.2285, 0, -16.8036, 20.0679, 0, -16.7232, 20.0679, 0, -16.7232, 23.412498, 0, -20.0679, 23.412498, 0, -16.7232, 18.0241, 0, -14.3019, 20.0679, 0, -16.7232, 21.1831, 0, -13.6544, 21.1831, 0, -13.6544, 20.0679, 0, -16.7232, 23.412498, 0, -16.7232, 21.1831, 0, -13.6544, 23.412498, 0, -16.7232, 23.412498, 0, -13.3786, 21.8888, 0, -10.033899, 21.1831, 0, -13.6544, 23.412498, 0, -13.3786, 21.8888, 0, -10.033899, 23.412498, 0, -13.3786, 23.412498, 0, -10.033899, 21.2349, 0, -6.6893, 21.8888, 0, -10.033899, 23.412498, 0, -10.033899, 21.2349, 0, -6.6893, 23.412498, 0, -10.033899, 23.412498, 0, -6.6893, 20.961199, 0, -3.3446, 21.2349, 0, -6.6893, 23.412498, 0, -6.6893, 20.961199, 0, -3.3446, 23.412498, 0, -6.6893, 23.412498, 0, -3.3446, 20.0679, 0, 0, 20.961199, 0, -3.3446, 23.412498, 0, -3.3446, 20.0679, 0, 0, 23.412498, 0, -3.3446, 23.412498, 0, 0, 20.0679, 0, 3.3446, 20.0679, 0, 0, 23.412498, 0, 0, 20.0679, 0, 3.3446, 23.412498, 0, 0, 23.412498, 0, 3.3446, 10.572599, 0, 16.7232, 13.8046, 0, 13.918799, 13.3786, 0, 16.7232, 10.572599, 0, 16.7232, 10.572599, 0, 13.918799, 13.8046, 0, 13.918799, 10.572599, 0, 20.0679, 10.572599, 0, 16.7232, 13.3786, 0, 16.7232, 13.3786, 0, 16.7232, 13.8046, 0, 13.918799, 15.0508995, 0, 13.3786, 10.572599, 0, 20.0679, 13.3786, 0, 16.7232, 13.3786, 0, 20.0679, 13.8046, 0, 13.918799, 15.0508995, 0, 10.033899, 15.0508995, 0, 13.3786, 13.8046, 0, 13.918799, 13.8046, 0, 10.033899, 15.0508995, 0, 10.033899, 13.8046, 0, 10.033899, 15.0508995, 0, 6.6893, 15.0508995, 0, 10.033899, 13.8046, 0, 10.033899, 13.8046, 0, 6.6893, 15.0508995, 0, 6.6893, 15.0508995, 0, 10.033899, 15.0508995, 0, 6.6893, 16.7232, 0, 6.6893, 15.0508995, 0, 10.033899, 16.7232, 0, 6.6893, 16.7232, 0, 10.033899, 15.0508995, 0, 13.3786, 15.0508995, 0, 10.033899, 16.7232, 0, 10.033899, 13.3786, 0, 16.7232, 15.0508995, 0, 13.3786, 15.0508995, 0, 16.7232, 13.3786, 0, 20.0679, 13.3786, 0, 16.7232, 15.0508995, 0, 16.7232, 15.0508995, 0, 13.3786, 16.7232, 0, 10.033899, 16.7232, 0, 13.3786, 15.0508995, 0, 16.7232, 15.0508995, 0, 13.3786, 16.7232, 0, 13.3786, 16.7232, 0, 13.3786, 16.7232, 0, 10.033899, 20.0679, 0, 10.033899, 16.7232, 0, 13.3786, 20.0679, 0, 10.033899, 20.0679, 0, 13.3786, 20.0679, 0, 13.3786, 20.0679, 0, 10.033899, 23.412498, 0, 10.033899, 20.0679, 0, 13.3786, 23.412498, 0, 10.033899, 23.412498, 0, 13.3786, 16.7232, 0, 16.7232, 16.7232, 0, 13.3786, 20.0679, 0, 13.3786, 15.0508995, 0, 16.7232, 16.7232, 0, 13.3786, 16.7232, 0, 16.7232, 20.0679, 0, 16.7232, 20.0679, 0, 13.3786, 23.412498, 0, 13.3786, 16.7232, 0, 16.7232, 20.0679, 0, 13.3786, 20.0679, 0, 16.7232, 20.0679, 0, 16.7232, 23.412498, 0, 13.3786, 23.412498, 0, 16.7232, 15.0508995, 0, 20.0679, 15.0508995, 0, 16.7232, 16.7232, 0, 16.7232, 13.3786, 0, 20.0679, 15.0508995, 0, 16.7232, 15.0508995, 0, 20.0679, 16.7232, 0, 20.0679, 16.7232, 0, 16.7232, 20.0679, 0, 16.7232, 15.0508995, 0, 20.0679, 16.7232, 0, 16.7232, 16.7232, 0, 20.0679, 20.0679, 0, 20.0679, 20.0679, 0, 16.7232, 23.412498, 0, 16.7232, 16.7232, 0, 20.0679, 20.0679, 0, 16.7232, 20.0679, 0, 20.0679, 20.0679, 0, 20.0679, 23.412498, 0, 16.7232, 23.412498, 0, 20.0679, 20.0679, 0, 23.412498, 20.0679, 0, 20.0679, 23.412498, 0, 20.0679, 20.0679, 0, 23.412498, 23.412498, 0, 20.0679, 23.412498, 0, 23.412498, 20.0679, 0, 26.7572, 20.0679, 0, 23.412498, 23.412498, 0, 23.412498, 20.0679, 0, 26.7572, 23.412498, 0, 23.412498, 23.412498, 0, 26.7572, 16.7232, 0, 26.7572, 20.0679, 0, 23.412498, 20.0679, 0, 26.7572, 16.7232, 0, 23.412498, 20.0679, 0, 20.0679, 20.0679, 0, 23.412498, 16.7232, 0, 26.7572, 16.7232, 0, 23.412498, 20.0679, 0, 23.412498, 16.7232, 0, 23.412498, 16.7232, 0, 20.0679, 20.0679, 0, 20.0679, 15.0508995, 0, 26.7572, 16.7232, 0, 23.412498, 16.7232, 0, 26.7572, 15.0508995, 0, 23.412498, 16.7232, 0, 20.0679, 16.7232, 0, 23.412498, 15.0508995, 0, 26.7572, 15.0508995, 0, 23.412498, 16.7232, 0, 23.412498, 15.0508995, 0, 23.412498, 15.0508995, 0, 20.0679, 16.7232, 0, 20.0679, 13.3786, 0, 26.7572, 15.0508995, 0, 23.412498, 15.0508995, 0, 26.7572, 13.3786, 0, 23.412498, 15.0508995, 0, 20.0679, 15.0508995, 0, 23.412498, 13.3786, 0, 26.7572, 13.3786, 0, 23.412498, 15.0508995, 0, 23.412498, 13.3786, 0, 23.412498, 13.3786, 0, 20.0679, 15.0508995, 0, 20.0679, 10.033899, 0, 26.7572, 13.3786, 0, 23.412498, 13.3786, 0, 26.7572, 10.033899, 0, 23.412498, 13.3786, 0, 20.0679, 13.3786, 0, 23.412498, 10.033899, 0, 26.7572, 10.033899, 0, 23.412498, 13.3786, 0, 23.412498, 10.033899, 0, 23.412498, 10.572599, 0, 20.0679, 13.3786, 0, 20.0679, 6.6893, 0, 26.7572, 10.033899, 0, 23.412498, 10.033899, 0, 26.7572, 6.6893, 0, 23.412498, 10.572599, 0, 20.0679, 10.033899, 0, 23.412498, 6.6893, 0, 26.7572, 6.6893, 0, 23.412498, 10.033899, 0, 23.412498, 6.6893, 0, 23.412498, 6.6893, 0, 20.0679, 10.572599, 0, 20.0679, 3.3446, 0, 26.7572, 6.6893, 0, 23.412498, 6.6893, 0, 26.7572, 3.3446, 0, 23.412498, 6.6893, 0, 20.0679, 6.6893, 0, 23.412498, 3.3446, 0, 26.7572, 3.3446, 0, 23.412498, 6.6893, 0, 23.412498, 3.3446, 0, 23.412498, 3.3446, 0, 20.0679, 6.6893, 0, 20.0679, 0, 0, 26.7572, 3.3446, 0, 23.412498, 3.3446, 0, 26.7572, 0, 0, 26.7572, 0, 0, 23.412498, 3.3446, 0, 23.412498, 0, 0, 23.412498, 3.3446, 0, 20.0679, 3.3446, 0, 23.412498, -3.3446, 0, 26.7572, 0, 0, 23.412498, 0, 0, 26.7572, 0, 0, 23.412498, 0, 0, 20.0679, 3.3446, 0, 20.0679, 0, 0, 20.0679, 3.3446, 0, 16.7232, 3.3446, 0, 20.0679, 0, 0, 20.0679, 0, 0, 16.7232, 3.3446, 0, 16.7232, -3.3446, 0, 26.7572, -3.3446, 0, 23.412498, 0, 0, 23.412498, -3.3446, 0, 23.412498, 0, 0, 20.0679, 0, 0, 23.412498, -6.6893, 0, 26.7572, -3.3446, 0, 23.412498, -3.3446, 0, 26.7572, -3.3446, 0, 20.0679, 0, 0, 16.7232, 0, 0, 20.0679, -3.3446, 0, 23.412498, -3.3446, 0, 20.0679, 0, 0, 20.0679, -3.3446, 0, 20.0679, -3.3446, 0, 16.7232, 0, 0, 16.7232, -6.6893, 0, 26.7572, -6.6893, 0, 23.412498, -3.3446, 0, 23.412498, -6.6893, 0, 23.412498, -3.3446, 0, 20.0679, -3.3446, 0, 23.412498, -10.033899, 0, 26.7572, -6.6893, 0, 23.412498, -6.6893, 0, 26.7572, -6.6893, 0, 20.0679, -3.3446, 0, 16.7232, -3.3446, 0, 20.0679, -6.6893, 0, 23.412498, -6.6893, 0, 20.0679, -3.3446, 0, 20.0679, -6.6893, 0, 20.0679, -6.6893, 0, 16.7232, -3.3446, 0, 16.7232, -10.033899, 0, 26.7572, -10.033899, 0, 23.412498, -6.6893, 0, 23.412498, -10.033899, 0, 23.412498, -6.6893, 0, 20.0679, -6.6893, 0, 23.412498, -13.3786, 0, 26.7572, -10.033899, 0, 23.412498, -10.033899, 0, 26.7572, -10.033899, 0, 20.0679, -6.6893, 0, 16.7232, -6.6893, 0, 20.0679, -10.033899, 0, 23.412498, -10.033899, 0, 20.0679, -6.6893, 0, 20.0679, -13.3786, 0, 26.7572, -13.3786, 0, 23.412498, -10.033899, 0, 23.412498, -13.3786, 0, 23.412498, -10.033899, 0, 20.0679, -10.033899, 0, 23.412498, -16.7232, 0, 26.7572, -13.3786, 0, 23.412498, -13.3786, 0, 26.7572, -10.033899, 0, 20.0679, -10.033899, 0, 16.7232, -6.6893, 0, 16.7232, -10.033899, 0, 16.7232, -6.6893, 0, 13.3786, -6.6893, 0, 16.7232, -10.033899, 0, 16.7232, -10.033899, 0, 13.3786, -6.6893, 0, 13.3786, -13.3786, 0, 20.0679, -10.033899, 0, 16.7232, -10.033899, 0, 20.0679, -13.3786, 0, 23.412498, -13.3786, 0, 20.0679, -10.033899, 0, 20.0679, -13.3786, 0, 16.7232, -10.033899, 0, 13.3786, -10.033899, 0, 16.7232, -13.3786, 0, 20.0679, -13.3786, 0, 16.7232, -10.033899, 0, 16.7232, -13.3786, 0, 16.7232, -13.3786, 0, 13.3786, -10.033899, 0, 13.3786, -16.7232, 0, 26.7572, -16.7232, 0, 23.412498, -13.3786, 0, 23.412498, -16.7232, 0, 23.412498, -13.3786, 0, 20.0679, -13.3786, 0, 23.412498, -20.0679, 0, 26.7572, -16.7232, 0, 23.412498, -16.7232, 0, 26.7572, -16.7232, 0, 20.0679, -13.3786, 0, 16.7232, -13.3786, 0, 20.0679, -16.7232, 0, 23.412498, -16.7232, 0, 20.0679, -13.3786, 0, 20.0679, -16.7232, 0, 16.7232, -13.3786, 0, 13.3786, -13.3786, 0, 16.7232, -16.7232, 0, 20.0679, -16.7232, 0, 16.7232, -13.3786, 0, 16.7232, -16.7232, 0, 16.7232, -16.7232, 0, 13.3786, -13.3786, 0, 13.3786, -20.0679, 0, 26.7572, -20.0679, 0, 23.412498, -16.7232, 0, 23.412498, -20.0679, 0, 23.412498, -16.7232, 0, 20.0679, -16.7232, 0, 23.412498, -23.412498, 0, 26.7572, -20.0679, 0, 23.412498, -20.0679, 0, 26.7572, -20.0679, 0, 20.0679, -16.7232, 0, 16.7232, -16.7232, 0, 20.0679, -20.0679, 0, 23.412498, -20.0679, 0, 20.0679, -16.7232, 0, 20.0679, -23.412498, 0, 26.7572, -23.412498, 0, 23.412498, -20.0679, 0, 23.412498, -23.412498, 0, 23.412498, -20.0679, 0, 20.0679, -20.0679, 0, 23.412498, -26.7572, 0, 26.7572, -23.412498, 0, 23.412498, -23.412498, 0, 26.7572, -26.7572, 0, 26.7572, -26.7572, 0, 23.412498, -23.412498, 0, 23.412498, -26.7572, 0, 23.412498, -23.412498, 0, 20.0679, -23.412498, 0, 23.412498, -23.412498, 0, 23.412498, -23.412498, 0, 20.0679, -20.0679, 0, 20.0679, -26.7572, 0, 23.412498, -26.7572, 0, 20.0679, -23.412498, 0, 20.0679, -20.0679, 0, 20.0679, -20.0679, 0, 16.7232, -16.7232, 0, 16.7232, -23.412498, 0, 20.0679, -20.0679, 0, 16.7232, -20.0679, 0, 20.0679, -20.0679, 0, 16.7232, -16.7232, 0, 13.3786, -16.7232, 0, 16.7232, -26.7572, 0, 20.0679, -23.412498, 0, 16.7232, -23.412498, 0, 20.0679, -23.412498, 0, 20.0679, -23.412498, 0, 16.7232, -20.0679, 0, 16.7232, -26.7572, 0, 20.0679, -26.7572, 0, 16.7232, -23.412498, 0, 16.7232, -20.0679, 0, 16.7232, -20.0679, 0, 13.3786, -16.7232, 0, 13.3786, -23.412498, 0, 16.7232, -20.0679, 0, 13.3786, -20.0679, 0, 16.7232, -20.0679, 0, 13.3786, -16.7232, 0, 10.033899, -16.7232, 0, 13.3786, -20.0679, 0, 13.3786, -20.0679, 0, 10.033899, -16.7232, 0, 10.033899, -23.412498, 0, 16.7232, -23.412498, 0, 13.3786, -20.0679, 0, 13.3786, -23.412498, 0, 13.3786, -20.0679, 0, 10.033899, -20.0679, 0, 13.3786, -26.7572, 0, 16.7232, -23.412498, 0, 13.3786, -23.412498, 0, 16.7232, -23.412498, 0, 13.3786, -23.412498, 0, 10.033899, -20.0679, 0, 10.033899, -26.7572, 0, 16.7232, -26.7572, 0, 13.3786, -23.412498, 0, 13.3786, -26.7572, 0, 13.3786, -23.412498, 0, 10.033899, -23.412498, 0, 13.3786, -26.7572, 0, 13.3786, -26.7572, 0, 10.033899, -23.412498, 0, 10.033899, -26.7572, 0, 10.033899, -23.8078, 0, 6.6893, -23.412498, 0, 10.033899, -26.7572, 0, 10.033899, -26.7572, 0, 6.6893, -23.8078, 0, 6.6893, -26.7572, 0, 6.6893, -23.412498, 0, 3.3446, -23.8078, 0, 6.6893, -26.7572, 0, 6.6893, -26.7572, 0, 3.3446, -23.412498, 0, 3.3446, -16.7232, 0, -23.412498, -13.3786, 0, -26.7572, -13.3786, 0, -23.412498, -13.3786, 0, -23.412498, -13.3786, 0, -26.7572, -10.033899, 0, -26.7572, -16.7232, 0, -23.412498, -16.7232, 0, -26.7572, -13.3786, 0, -26.7572, -13.3786, 0, -23.412498, -10.033899, 0, -26.7572, -10.033899, 0, -23.412498, -10.033899, 0, -23.412498, -10.033899, 0, -26.7572, -6.6893, 0, -26.7572, -16.7232, 0, -20.0679, -16.7232, 0, -23.412498, -13.3786, 0, -23.412498, -10.033899, 0, -23.412498, -6.6893, 0, -26.7572, -6.6893, 0, -23.412498, -6.6893, 0, -23.412498, -6.6893, 0, -26.7572, -3.3446, 0, -26.7572, -6.6893, 0, -23.412498, -3.3446, 0, -26.7572, -3.3446, 0, -23.412498, -13.3786, 0, -20.0679, -13.3786, 0, -23.412498, -10.033899, 0, -23.412498, -16.7232, 0, -20.0679, -13.3786, 0, -23.412498, -13.3786, 0, -20.0679, -10.033899, 0, -20.0679, -10.033899, 0, -23.412498, -6.6893, 0, -23.412498, -13.3786, 0, -20.0679, -10.033899, 0, -23.412498, -10.033899, 0, -20.0679, -6.6893, 0, -20.0679, -6.6893, 0, -23.412498, -3.3446, 0, -23.412498, -10.033899, 0, -20.0679, -6.6893, 0, -23.412498, -6.6893, 0, -20.0679, -6.6893, 0, -20.0679, -3.3446, 0, -23.412498, -3.3446, 0, -20.0679, -13.3786, 0, -17.3364, -13.3786, 0, -20.0679, -10.033899, 0, -20.0679, -13.3786, 0, -17.3364, -10.033899, 0, -20.0679, -10.033899, 0, -16.7232, -10.033899, 0, -16.7232, -10.033899, 0, -20.0679, -6.6893, 0, -20.0679, -16.7232, 0, -16.7232, -13.3786, 0, -20.0679, -13.3786, 0, -17.3364, -16.7232, 0, -16.7232, -16.7232, 0, -20.0679, -13.3786, 0, -20.0679, -10.033899, 0, -16.7232, -6.6893, 0, -20.0679, -6.6893, 0, -16.7232, -6.6893, 0, -16.7232, -6.6893, 0, -20.0679, -3.3446, 0, -20.0679, -9.6168995, 0, -14.127899, -10.033899, 0, -16.7232, -6.6893, 0, -16.7232, -9.6168995, 0, -14.127899, -6.6893, 0, -16.7232, -6.6893, 0, -14.2637, -6.6893, 0, -16.7232, -3.3446, 0, -20.0679, -3.3446, 0, -16.7232, -6.6893, 0, -14.2637, -6.6893, 0, -16.7232, -3.3446, 0, -16.7232, -3.3446, 0, -16.7232, -3.3446, 0, -20.0679, 0, 0, -20.0679, -6.6893, 0, -14.2637, -3.3446, 0, -16.7232, -3.3446, 0, -13.3786, -3.3446, 0, -16.7232, 0, 0, -20.0679, 0, 0, -16.7232, -3.3446, 0, -13.3786, -3.3446, 0, -16.7232, 0, 0, -16.7232, 0, 0, -16.7232, 0, 0, -20.0679, 3.3446, 0, -20.5992, 0, 0, -16.7232, 3.3446, 0, -20.5992, 3.0140998, 0, -16.7232, -3.3446, 0, -13.3786, 0, 0, -16.7232, 0, 0, -13.3786, -2.6297, 0, -10.5745, -3.3446, 0, -13.3786, 0, 0, -13.3786, -2.6297, 0, -10.5745, 0, 0, -13.3786, 0, 0, -10.7654, -20.0679, 0, -16.7232, -16.7232, 0, -20.0679, -16.7232, 0, -16.7232, -20.0679, 0, -16.7232, -20.0679, 0, -20.0679, -16.7232, 0, -20.0679, -20.0679, 0, -20.0679, -16.7232, 0, -23.412498, -16.7232, 0, -20.0679, -20.0679, 0, -13.3786, -20.0679, 0, -16.7232, -16.7232, 0, -16.7232, -20.0679, 0, -13.3786, -16.7232, 0, -16.7232, -16.7232, 0, -13.3786, -23.412498, 0, -16.7232, -20.0679, 0, -20.0679, -20.0679, 0, -16.7232, -20.0679, 0, -10.033899, -20.0679, 0, -13.3786, -16.7232, 0, -13.3786, -20.0679, 0, -10.033899, -16.7232, 0, -13.3786, -17.275, 0, -10.5169, -23.412498, 0, -13.3786, -20.0679, 0, -16.7232, -20.0679, 0, -13.3786, -23.412498, 0, -13.3786, -23.412498, 0, -16.7232, -20.0679, 0, -16.7232, -23.412498, 0, -10.033899, -20.0679, 0, -13.3786, -20.0679, 0, -10.033899, -23.412498, 0, -10.033899, -23.412498, 0, -13.3786, -20.0679, 0, -13.3786, -26.7572, 0, -10.033899, -23.412498, 0, -13.3786, -23.412498, 0, -10.033899, -26.7572, 0, -10.033899, -26.7572, 0, -13.3786, -23.412498, 0, -13.3786, -26.7572, 0, -13.3786, -23.412498, 0, -16.7232, -23.412498, 0, -13.3786, -26.7572, 0, -6.6893, -26.7572, 0, -10.033899, -23.412498, 0, -10.033899, -26.7572, 0, -13.3786, -26.7572, 0, -16.7232, -23.412498, 0, -16.7232, -26.7572, 0, -6.6893, -23.412498, 0, -10.033899, -23.412498, 0, -6.6893, -23.412498, 0, -6.6893, -23.412498, 0, -10.033899, -20.0679, 0, -10.033899, -23.412498, 0, -6.6893, -20.0679, 0, -10.033899, -20.3764, 0, -6.6893, -26.7572, 0, -3.3446, -26.7572, 0, -6.6893, -23.412498, 0, -6.6893, -26.7572, 0, -3.3446, -23.412498, 0, -6.6893, -23.412498, 0, -3.3446, -26.7572, 0, -16.7232, -23.412498, 0, -20.0679, -23.412498, 0, -16.7232, -23.412498, 0, -16.7232, -23.412498, 0, -20.0679, -20.0679, 0, -20.0679, -26.7572, 0, -16.7232, -26.7572, 0, -20.0679, -23.412498, 0, -20.0679, -23.412498, 0, -20.0679, -20.0679, 0, -23.412498, -20.0679, 0, -20.0679, -20.0679, 0, -20.0679, -20.0679, 0, -23.412498, -16.7232, 0, -23.412498, -20.0679, 0, -23.412498, -16.7232, 0, -26.7572, -16.7232, 0, -23.412498, -20.0679, 0, -23.412498, -20.0679, 0, -26.7572, -16.7232, 0, -26.7572, -23.412498, 0, -20.0679, -23.412498, 0, -23.412498, -20.0679, 0, -23.412498, -23.412498, 0, -23.412498, -20.0679, 0, -26.7572, -20.0679, 0, -23.412498, -26.7572, 0, -20.0679, -23.412498, 0, -23.412498, -23.412498, 0, -20.0679, -23.412498, 0, -23.412498, -23.412498, 0, -26.7572, -20.0679, 0, -26.7572, -26.7572, 0, -20.0679, -26.7572, 0, -23.412498, -23.412498, 0, -23.412498, -26.7572, 0, -23.412498, -23.412498, 0, -26.7572, -23.412498, 0, -23.412498, -26.7572, 0, -23.412498, -26.7572, 0, -26.7572, -23.412498, 0, -26.7572, -23.412498, 0, -3.3446, -20.3764, 0, -6.6893, -20.0679, -0.3484, -3.5669, -23.412498, 0, -3.3446, -23.412498, 0, -6.6893, -20.3764, 0, -6.6893, 16.7232, 0, 10.033899, 20.0679, -0.124199994, 6.6893, 20.0679, 0, 10.033899, 16.7232, 0, 10.033899, 16.7232, 0, 6.6893, 20.0679, -0.124199994, 6.6893, 20.0679, 0, 10.033899, 23.412498, 0, 6.6893, 23.412498, 0, 10.033899, 20.0679, 0, 10.033899, 20.0679, -0.124199994, 6.6893, 23.412498, 0, 6.6893, -26.7572, 0, 3.3446, -23.412498, -0.2913, 0.26119998, -23.412498, 0, 3.3446, -26.7572, 0, 3.3446, -26.7572, 0, 0, -23.412498, -0.2913, 0.26119998, -26.7572, 0, 0, -23.412498, 0, -3.3446, -23.412498, -0.2913, 0.26119998, -26.7572, 0, 0, -26.7572, 0, -3.3446, -23.412498, 0, -3.3446, 0, 0, -13.3786, 3.0140998, 0, -16.7232, 3.3446, -0.3989, -12.9443, 0, 0, -13.3786, 0, 0, -16.7232, 3.0140998, 0, -16.7232, 0, 0, -10.7654, 3.3446, -0.3989, -12.9443, 2.8241, 0, -10.4594, 0, 0, -10.7654, 0, 0, -13.3786, 3.3446, -0.3989, -12.9443, 20.0679, -0.124199994, 6.6893, 23.412498, 0, 3.3446, 23.412498, 0, 6.6893, 20.0679, -0.124199994, 6.6893, 20.0679, 0, 3.3446, 23.412498, 0, 3.3446, 51.2184, -3.9413, -15.0508995, 54.072197, -3.9413, -13.3786, 51.2184, -3.9413, -13.3786, 51.2184, -3.9413, -13.3786, 54.072197, -3.9413, -13.3786, 54.072197, -3.9413, -11.7063, 51.2184, -3.9413, -15.0508995, 54.072197, -3.9413, -15.0508995, 54.072197, -3.9413, -13.3786, 51.2184, -3.9413, -13.3786, 54.072197, -3.9413, -11.7063, 51.2184, -3.9413, -11.7063, 51.2184, -3.9413, -11.7063, 54.072197, -3.9413, -11.7063, 54.072197, -3.9413, -10.033899, 48.3645, -3.9413, -15.0508995, 51.2184, -3.9413, -15.0508995, 51.2184, -3.9413, -13.3786, 51.2184, -3.9413, -11.7063, 54.072197, -3.9413, -10.033899, 51.2184, -3.9413, -10.033899, 51.2184, -3.9413, -10.033899, 54.072197, -3.9413, -10.033899, 54.072197, -3.9413, -8.3616, 48.3645, -3.9413, -13.3786, 51.2184, -3.9413, -13.3786, 51.2184, -3.9413, -11.7063, 48.3645, -3.9413, -15.0508995, 51.2184, -3.9413, -13.3786, 48.3645, -3.9413, -13.3786, 48.3645, -3.9413, -11.7063, 51.2184, -3.9413, -11.7063, 51.2184, -3.9413, -10.033899, 48.3645, -3.9413, -13.3786, 51.2184, -3.9413, -11.7063, 48.3645, -3.9413, -11.7063, 51.2184, -3.9413, -10.033899, 54.072197, -3.9413, -8.3616, 51.2184, -3.9413, -8.3616, 51.2184, -3.9413, -8.3616, 54.072197, -3.9413, -8.3616, 54.072197, -3.9413, -6.6893, 48.3645, -3.9413, -11.7063, 51.2184, -3.9413, -10.033899, 48.3645, -3.9413, -10.033899, 48.3645, -3.9413, -10.033899, 51.2184, -3.9413, -10.033899, 51.2184, -3.9413, -8.3616, 45.5107, -3.9413, -13.3786, 48.3645, -3.9413, -13.3786, 48.3645, -3.9413, -11.7063, 45.5107, -3.9413, -11.7063, 48.3645, -3.9413, -11.7063, 48.3645, -3.9413, -10.033899, 45.5107, -3.9413, -13.3786, 48.3645, -3.9413, -11.7063, 45.5107, -3.9413, -11.7063, 51.2184, -3.9413, -8.3616, 54.072197, -3.9413, -6.6893, 51.2184, -3.9413, -6.6893, 51.2184, -3.9413, -6.6893, 54.072197, -3.9413, -6.6893, 54.072197, -3.9413, -5.0169997, 48.3645, -3.9413, -10.033899, 51.2184, -3.9413, -8.3616, 48.3645, -3.9413, -8.3616, 48.3645, -3.9413, -8.3616, 51.2184, -3.9413, -8.3616, 51.2184, -3.9413, -6.6893, 45.5107, -3.9413, -11.7063, 48.3645, -3.9413, -10.033899, 45.5107, -3.9413, -10.033899, 45.5107, -3.9413, -10.033899, 48.3645, -3.9413, -10.033899, 48.3645, -3.9413, -8.3616, 51.2184, -3.9413, -6.6893, 54.072197, -3.9413, -5.0169997, 51.2184, -3.9413, -5.0169997, 51.2184, -3.9413, -5.0169997, 54.072197, -3.9413, -5.0169997, 54.072197, -3.9413, -3.3446, 48.3645, -3.9413, -8.3616, 51.2184, -3.9413, -6.6893, 48.3645, -3.9413, -6.6893, 48.3645, -3.9413, -6.6893, 51.2184, -3.9413, -6.6893, 51.2184, -3.9413, -5.0169997, 45.5107, -3.9413, -10.033899, 48.3645, -3.9413, -8.3616, 45.5107, -3.9413, -8.3616, 45.5107, -3.9413, -8.3616, 48.3645, -3.9413, -8.3616, 48.3645, -3.9413, -6.6893, 51.2184, -3.9413, -5.0169997, 54.072197, -3.9413, -3.3446, 51.2184, -3.9413, -3.3446, 51.2184, -3.9413, -3.3446, 54.072197, -3.9413, -3.3446, 54.072197, -3.9413, -1.6723, 48.3645, -3.9413, -6.6893, 51.2184, -3.9413, -5.0169997, 48.3645, -3.9413, -5.0169997, 48.3645, -3.9413, -5.0169997, 51.2184, -3.9413, -5.0169997, 51.2184, -3.9413, -3.3446, 45.5107, -3.9413, -8.3616, 48.3645, -3.9413, -6.6893, 45.5107, -3.9413, -6.6893, 45.5107, -3.9413, -6.6893, 48.3645, -3.9413, -6.6893, 48.3645, -3.9413, -5.0169997, 51.2184, -3.9413, -3.3446, 54.072197, -3.9413, -1.6723, 51.2184, -3.9413, -1.6723, 51.2184, -3.9413, -1.6723, 54.072197, -3.9413, -1.6723, 54.072197, -3.9413, 0, 48.3645, -3.9413, -5.0169997, 51.2184, -3.9413, -3.3446, 48.3645, -3.9413, -3.3446, 48.3645, -3.9413, -3.3446, 51.2184, -3.9413, -3.3446, 51.2184, -3.9413, -1.6723, 45.5107, -3.9413, -6.6893, 48.3645, -3.9413, -5.0169997, 45.5107, -3.9413, -5.0169997, 45.5107, -3.9413, -5.0169997, 48.3645, -3.9413, -5.0169997, 48.3645, -3.9413, -3.3446, 51.2184, -3.9413, -1.6723, 54.072197, -3.9413, 0, 51.2184, -3.9413, 0, 51.2184, -3.9413, 0, 54.072197, -3.9413, 0, 54.072197, -3.9413, 1.6723, 48.3645, -3.9413, -3.3446, 51.2184, -3.9413, -1.6723, 48.3645, -3.9413, -1.6723, 48.3645, -3.9413, -1.6723, 51.2184, -3.9413, -1.6723, 51.2184, -3.9413, 0, 45.5107, -3.9413, -5.0169997, 48.3645, -3.9413, -3.3446, 45.5107, -3.9413, -3.3446, 45.5107, -3.9413, -3.3446, 48.3645, -3.9413, -3.3446, 48.3645, -3.9413, -1.6723, 51.2184, -3.9413, 0, 54.072197, -3.9413, 1.6723, 51.2184, -3.9413, 1.6723, 51.2184, -3.9413, 1.6723, 54.072197, -3.9413, 1.6723, 54.072197, -3.9413, 3.3446, 48.3645, -3.9413, -1.6723, 51.2184, -3.9413, 0, 48.3645, -3.9413, 0, 48.3645, -3.9413, 0, 51.2184, -3.9413, 0, 51.2184, -3.9413, 1.6723, 45.5107, -3.9413, -3.3446, 48.3645, -3.9413, -1.6723, 45.5107, -3.9413, -1.6723, 45.5107, -3.9413, -1.6723, 48.3645, -3.9413, -1.6723, 48.3645, -3.9413, 0, 51.2184, -3.9413, 1.6723, 54.072197, -3.9413, 3.3446, 51.2184, -3.9413, 3.3446, 51.2184, -3.9413, 3.3446, 54.072197, -3.9413, 3.3446, 54.072197, -3.9413, 5.0169997, 48.3645, -3.9413, 0, 51.2184, -3.9413, 1.6723, 48.3645, -3.9413, 1.6723, 48.3645, -3.9413, 1.6723, 51.2184, -3.9413, 1.6723, 51.2184, -3.9413, 3.3446, 45.5107, -3.9413, -1.6723, 48.3645, -3.9413, 0, 45.5107, -3.9413, 0, 45.5107, -3.9413, 0, 48.3645, -3.9413, 0, 48.3645, -3.9413, 1.6723, 51.2184, -3.9413, 3.3446, 54.072197, -3.9413, 5.0169997, 51.2184, -3.9413, 5.0169997, 51.2184, -3.9413, 5.0169997, 54.072197, -3.9413, 5.0169997, 54.072197, -3.9413, 6.6893, 48.3645, -3.9413, 1.6723, 51.2184, -3.9413, 3.3446, 48.3645, -3.9413, 3.3446, 48.3645, -3.9413, 3.3446, 51.2184, -3.9413, 3.3446, 51.2184, -3.9413, 5.0169997, 45.5107, -3.9413, 0, 48.3645, -3.9413, 1.6723, 45.5107, -3.9413, 1.6723, 45.5107, -3.9413, 1.6723, 48.3645, -3.9413, 1.6723, 48.3645, -3.9413, 3.3446, 51.2184, -3.9413, 5.0169997, 54.072197, -3.9413, 6.6893, 51.2184, -3.9413, 6.6893, 51.2184, -3.9413, 6.6893, 54.072197, -3.9413, 6.6893, 54.072197, -3.9413, 8.3616, 48.3645, -3.9413, 3.3446, 51.2184, -3.9413, 5.0169997, 48.3645, -3.9413, 5.0169997, 48.3645, -3.9413, 5.0169997, 51.2184, -3.9413, 5.0169997, 51.2184, -3.9413, 6.6893, 45.5107, -3.9413, 1.6723, 48.3645, -3.9413, 3.3446, 45.5107, -3.9413, 3.3446, 45.5107, -3.9413, 3.3446, 48.3645, -3.9413, 3.3446, 48.3645, -3.9413, 5.0169997, 51.2184, -3.9413, 6.6893, 54.072197, -3.9413, 8.3616, 51.2184, -3.9413, 8.3616, 51.2184, -3.9413, 8.3616, 54.072197, -3.9413, 8.3616, 54.072197, -3.9413, 10.033899, 48.3645, -3.9413, 5.0169997, 51.2184, -3.9413, 6.6893, 48.3645, -3.9413, 6.6893, 48.3645, -3.9413, 6.6893, 51.2184, -3.9413, 6.6893, 51.2184, -3.9413, 8.3616, 45.5107, -3.9413, 3.3446, 48.3645, -3.9413, 5.0169997, 45.5107, -3.9413, 5.0169997, 45.5107, -3.9413, 5.0169997, 48.3645, -3.9413, 5.0169997, 48.3645, -3.9413, 6.6893, 51.2184, -3.9413, 8.3616, 54.072197, -3.9413, 10.033899, 51.2184, -3.9413, 10.033899, 51.2184, -3.9413, 10.033899, 54.072197, -3.9413, 10.033899, 54.072197, -3.9413, 11.7063, 48.3645, -3.9413, 6.6893, 51.2184, -3.9413, 8.3616, 48.3645, -3.9413, 8.3616, 48.3645, -3.9413, 8.3616, 51.2184, -3.9413, 8.3616, 51.2184, -3.9413, 10.033899, 45.5107, -3.9413, 5.0169997, 48.3645, -3.9413, 6.6893, 45.5107, -3.9413, 6.6893, 45.5107, -3.9413, 6.6893, 48.3645, -3.9413, 6.6893, 48.3645, -3.9413, 8.3616, 51.2184, -3.9413, 10.033899, 54.072197, -3.9413, 11.7063, 51.2184, -3.9413, 11.7063, 51.2184, -3.9413, 11.7063, 54.072197, -3.9413, 11.7063, 54.072197, -3.9413, 13.3786, 48.3645, -3.9413, 8.3616, 51.2184, -3.9413, 10.033899, 48.3645, -3.9413, 10.033899, 48.3645, -3.9413, 10.033899, 51.2184, -3.9413, 10.033899, 51.2184, -3.9413, 11.7063, 45.5107, -3.9413, 6.6893, 48.3645, -3.9413, 8.3616, 45.5107, -3.9413, 8.3616, 45.5107, -3.9413, 8.3616, 48.3645, -3.9413, 8.3616, 48.3645, -3.9413, 10.033899, 51.2184, -3.9413, 11.7063, 54.072197, -3.9413, 13.3786, 51.2184, -3.9413, 13.3786, 51.2184, -3.9413, 13.3786, 54.072197, -3.9413, 13.3786, 54.072197, -3.9413, 15.0508995, 48.3645, -3.9413, 10.033899, 51.2184, -3.9413, 11.7063, 48.3645, -3.9413, 11.7063, 48.3645, -3.9413, 11.7063, 51.2184, -3.9413, 11.7063, 51.2184, -3.9413, 13.3786, 45.5107, -3.9413, 8.3616, 48.3645, -3.9413, 10.033899, 45.5107, -3.9413, 10.033899, 45.5107, -3.9413, 10.033899, 48.3645, -3.9413, 10.033899, 48.3645, -3.9413, 11.7063, 51.2184, -3.9413, 13.3786, 54.072197, -3.9413, 15.0508995, 51.2184, -3.9413, 15.0508995, 51.2184, -3.9413, 15.0508995, 54.072197, -3.9413, 15.0508995, 54.072197, -3.9413, 16.7232, 48.3645, -3.9413, 11.7063, 51.2184, -3.9413, 13.3786, 48.3645, -3.9413, 13.3786, 48.3645, -3.9413, 13.3786, 51.2184, -3.9413, 13.3786, 51.2184, -3.9413, 15.0508995, 45.5107, -3.9413, 10.033899, 48.3645, -3.9413, 11.7063, 45.5107, -3.9413, 11.7063, 45.5107, -3.9413, 11.7063, 48.3645, -3.9413, 11.7063, 48.3645, -3.9413, 13.3786, 51.2184, -3.9413, 15.0508995, 54.072197, -3.9413, 16.7232, 51.2184, -3.9413, 16.7232, 51.2184, -3.9413, 16.7232, 54.072197, -3.9413, 16.7232, 54.072197, -3.9413, 18.3955, 48.3645, -3.9413, 13.3786, 51.2184, -3.9413, 15.0508995, 48.3645, -3.9413, 15.0508995, 48.3645, -3.9413, 15.0508995, 51.2184, -3.9413, 15.0508995, 51.2184, -3.9413, 16.7232, 45.5107, -3.9413, 11.7063, 48.3645, -3.9413, 13.3786, 45.5107, -3.9413, 13.3786, 45.5107, -3.9413, 13.3786, 48.3645, -3.9413, 13.3786, 48.3645, -3.9413, 15.0508995, 51.2184, -3.9413, 16.7232, 54.072197, -3.9413, 18.3955, 51.2184, -3.9413, 18.3955, 51.2184, -3.9413, 18.3955, 54.072197, -3.9413, 18.3955, 54.072197, -3.9413, 20.0679, 48.3645, -3.9413, 15.0508995, 51.2184, -3.9413, 16.7232, 48.3645, -3.9413, 16.7232, 48.3645, -3.9413, 16.7232, 51.2184, -3.9413, 16.7232, 51.2184, -3.9413, 18.3955, 45.5107, -3.9413, 13.3786, 48.3645, -3.9413, 15.0508995, 45.5107, -3.9413, 15.0508995, 45.5107, -3.9413, 15.0508995, 48.3645, -3.9413, 15.0508995, 48.3645, -3.9413, 16.7232, 51.2184, -3.9413, 18.3955, 54.072197, -3.9413, 20.0679, 51.2184, -3.9413, 20.0679, 51.2184, -3.9413, 20.0679, 54.072197, -3.9413, 20.0679, 54.072197, -3.9413, 21.7402, 48.3645, -3.9413, 16.7232, 51.2184, -3.9413, 18.3955, 48.3645, -3.9413, 18.3955, 48.3645, -3.9413, 18.3955, 51.2184, -3.9413, 18.3955, 51.2184, -3.9413, 20.0679, 45.5107, -3.9413, 15.0508995, 48.3645, -3.9413, 16.7232, 45.5107, -3.9413, 16.7232, 45.5107, -3.9413, 16.7232, 48.3645, -3.9413, 16.7232, 48.3645, -3.9413, 18.3955, 51.2184, -3.9413, 20.0679, 54.072197, -3.9413, 21.7402, 51.2184, -3.9413, 21.7402, 51.2184, -3.9413, 21.7402, 54.072197, -3.9413, 21.7402, 54.072197, -3.9413, 23.412498, 48.3645, -3.9413, 18.3955, 51.2184, -3.9413, 20.0679, 48.3645, -3.9413, 20.0679, 48.3645, -3.9413, 20.0679, 51.2184, -3.9413, 20.0679, 51.2184, -3.9413, 21.7402, 45.5107, -3.9413, 16.7232, 48.3645, -3.9413, 18.3955, 45.5107, -3.9413, 18.3955, 45.5107, -3.9413, 18.3955, 48.3645, -3.9413, 18.3955, 48.3645, -3.9413, 20.0679, 51.2184, -3.9413, 21.7402, 54.072197, -3.9413, 23.412498, 51.2184, -3.9413, 23.412498, 51.2184, -3.9413, 23.412498, 54.072197, -3.9413, 23.412498, 54.072197, -3.9413, 25.084799, 48.3645, -3.9413, 20.0679, 51.2184, -3.9413, 21.7402, 48.3645, -3.9413, 21.7402, 48.3645, -3.9413, 21.7402, 51.2184, -3.9413, 21.7402, 51.2184, -3.9413, 23.412498, 45.5107, -3.9413, 18.3955, 48.3645, -3.9413, 20.0679, 45.5107, -3.9413, 20.0679, 45.5107, -3.9413, 20.0679, 48.3645, -3.9413, 20.0679, 48.3645, -3.9413, 21.7402, 51.2184, -3.9413, 23.412498, 54.072197, -3.9413, 25.084799, 51.2184, -3.9413, 25.084799, 51.2184, -3.9413, 25.084799, 54.072197, -3.9413, 25.084799, 54.072197, -3.9413, 26.7572, 51.2184, -3.9413, 25.084799, 54.072197, -3.9413, 26.7572, 51.2184, -3.9413, 26.7572, 48.3645, -3.9413, 23.412498, 51.2184, -3.9413, 23.412498, 51.2184, -3.9413, 25.084799, 48.3645, -3.9413, 21.7402, 51.2184, -3.9413, 23.412498, 48.3645, -3.9413, 23.412498, 48.3645, -3.9413, 25.084799, 51.2184, -3.9413, 25.084799, 51.2184, -3.9413, 26.7572, 48.3645, -3.9413, 23.412498, 51.2184, -3.9413, 25.084799, 48.3645, -3.9413, 25.084799, 48.3645, -3.9413, 25.084799, 51.2184, -3.9413, 26.7572, 48.3645, -3.9413, 26.7572, 45.5107, -3.9413, 21.7402, 48.3645, -3.9413, 21.7402, 48.3645, -3.9413, 23.412498, 45.5107, -3.9413, 20.0679, 48.3645, -3.9413, 21.7402, 45.5107, -3.9413, 21.7402, 45.5107, -3.9413, 23.412498, 48.3645, -3.9413, 23.412498, 48.3645, -3.9413, 25.084799, 45.5107, -3.9413, 21.7402, 48.3645, -3.9413, 23.412498, 45.5107, -3.9413, 23.412498, 45.5107, -3.9413, 25.084799, 48.3645, -3.9413, 25.084799, 48.3645, -3.9413, 26.7572, 45.5107, -3.9413, 23.412498, 48.3645, -3.9413, 25.084799, 45.5107, -3.9413, 25.084799, 45.5107, -3.9413, 25.084799, 48.3645, -3.9413, 26.7572, 45.5107, -3.9413, 26.7572, 42.6568, -3.9413, 25.084799, 45.5107, -3.9413, 25.084799, 45.5107, -3.9413, 26.7572, 42.6568, -3.9413, 25.084799, 45.5107, -3.9413, 26.7572, 42.6568, -3.9413, 26.7572, 39.7973, -3.9413, 25.084799, 42.6568, -3.9413, 25.084799, 42.6568, -3.9413, 26.7572, 39.7973, -3.9413, 25.084799, 42.6568, -3.9413, 26.7572, 39.7973, -3.9413, 26.7572, 39.7973, -3.9413, 23.412498, 42.6568, -3.9413, 25.084799, 39.7973, -3.9413, 25.084799, 42.6568, -3.9413, 23.412498, 45.5107, -3.9413, 25.084799, 42.6568, -3.9413, 25.084799, 39.7973, -3.9413, 23.412498, 42.6568, -3.9413, 23.412498, 42.6568, -3.9413, 25.084799, 42.6568, -3.9413, 23.412498, 45.5107, -3.9413, 23.412498, 45.5107, -3.9413, 25.084799, 39.7973, -3.9413, 21.7402, 42.6568, -3.9413, 23.412498, 39.7973, -3.9413, 23.412498, 42.6568, -3.9413, 21.7402, 45.5107, -3.9413, 23.412498, 42.6568, -3.9413, 23.412498, 39.7973, -3.9413, 21.7402, 42.6568, -3.9413, 21.7402, 42.6568, -3.9413, 23.412498, 42.6568, -3.9413, 21.7402, 45.5107, -3.9413, 21.7402, 45.5107, -3.9413, 23.412498, 39.7973, -3.9413, 20.0679, 42.6568, -3.9413, 21.7402, 39.7973, -3.9413, 21.7402, 42.6568, -3.9413, 20.0679, 45.5107, -3.9413, 21.7402, 42.6568, -3.9413, 21.7402, 39.7973, -3.9413, 20.0679, 42.6568, -3.9413, 20.0679, 42.6568, -3.9413, 21.7402, 42.6568, -3.9413, 20.0679, 45.5107, -3.9413, 20.0679, 45.5107, -3.9413, 21.7402, 39.7973, -3.9413, 18.3955, 42.6568, -3.9413, 20.0679, 39.7973, -3.9413, 20.0679, 42.6568, -3.9413, 18.3955, 45.5107, -3.9413, 20.0679, 42.6568, -3.9413, 20.0679, 39.7973, -3.9413, 18.3955, 42.6568, -3.9413, 18.3955, 42.6568, -3.9413, 20.0679, 42.6568, -3.9413, 18.3955, 45.5107, -3.9413, 18.3955, 45.5107, -3.9413, 20.0679, 39.7973, -3.9413, 16.7232, 42.6568, -3.9413, 18.3955, 39.7973, -3.9413, 18.3955, 42.6568, -3.9413, 16.7232, 45.5107, -3.9413, 18.3955, 42.6568, -3.9413, 18.3955, 39.7973, -3.9413, 16.7232, 42.6568, -3.9413, 16.7232, 42.6568, -3.9413, 18.3955, 42.6568, -3.9413, 16.7232, 45.5107, -3.9413, 16.7232, 45.5107, -3.9413, 18.3955, 39.7973, -3.9413, 15.0508995, 42.6568, -3.9413, 16.7232, 39.7973, -3.9413, 16.7232, 42.6568, -3.9413, 15.0508995, 45.5107, -3.9413, 16.7232, 42.6568, -3.9413, 16.7232, 39.7973, -3.9413, 15.0508995, 42.6568, -3.9413, 15.0508995, 42.6568, -3.9413, 16.7232, 42.6568, -3.9413, 15.0508995, 45.5107, -3.9413, 15.0508995, 45.5107, -3.9413, 16.7232, 39.7973, -3.9413, 13.3786, 42.6568, -3.9413, 15.0508995, 39.7973, -3.9413, 15.0508995, 42.6568, -3.9413, 13.3786, 45.5107, -3.9413, 15.0508995, 42.6568, -3.9413, 15.0508995, 39.7973, -3.9413, 13.3786, 42.6568, -3.9413, 13.3786, 42.6568, -3.9413, 15.0508995, 42.6568, -3.9413, 13.3786, 45.5107, -3.9413, 13.3786, 45.5107, -3.9413, 15.0508995, 39.7973, -3.9413, 11.7063, 42.6568, -3.9413, 13.3786, 39.7973, -3.9413, 13.3786, 42.6568, -3.9413, 11.7063, 45.5107, -3.9413, 13.3786, 42.6568, -3.9413, 13.3786, 39.7973, -3.9413, 11.7063, 42.6568, -3.9413, 11.7063, 42.6568, -3.9413, 13.3786, 42.6568, -3.9413, 11.7063, 45.5107, -3.9413, 11.7063, 45.5107, -3.9413, 13.3786, 39.7973, -3.9413, 10.033899, 42.6568, -3.9413, 11.7063, 39.7973, -3.9413, 11.7063, 42.6568, -3.9413, 10.033899, 45.5107, -3.9413, 11.7063, 42.6568, -3.9413, 11.7063, 39.7973, -3.9413, 10.033899, 42.6568, -3.9413, 10.033899, 42.6568, -3.9413, 11.7063, 42.6568, -3.9413, 10.033899, 45.5107, -3.9413, 10.033899, 45.5107, -3.9413, 11.7063, 39.7973, -3.9413, 8.3616, 42.6568, -3.9413, 10.033899, 39.7973, -3.9413, 10.033899, 42.6568, -3.9413, 8.3616, 45.5107, -3.9413, 10.033899, 42.6568, -3.9413, 10.033899, 39.7973, -3.9413, 8.3616, 42.6568, -3.9413, 8.3616, 42.6568, -3.9413, 10.033899, 42.6568, -3.9413, 8.3616, 45.5107, -3.9413, 8.3616, 45.5107, -3.9413, 10.033899, 39.7973, -3.9413, 6.6893, 42.6568, -3.9413, 8.3616, 39.7973, -3.9413, 8.3616, 42.6568, -3.9413, 6.6893, 45.5107, -3.9413, 8.3616, 42.6568, -3.9413, 8.3616, 39.7973, -3.9413, 6.6893, 42.6568, -3.9413, 6.6893, 42.6568, -3.9413, 8.3616, 42.6568, -3.9413, 6.6893, 45.5107, -3.9413, 6.6893, 45.5107, -3.9413, 8.3616, 39.7973, -3.9413, 5.0169997, 42.6568, -3.9413, 6.6893, 39.7973, -3.9413, 6.6893, 42.6568, -3.9413, 5.0169997, 45.5107, -3.9413, 6.6893, 42.6568, -3.9413, 6.6893, 39.7973, -3.9413, 5.0169997, 42.6568, -3.9413, 5.0169997, 42.6568, -3.9413, 6.6893, 42.6568, -3.9413, 5.0169997, 45.5107, -3.9413, 5.0169997, 45.5107, -3.9413, 6.6893, 39.7973, -3.9413, 3.3446, 42.6568, -3.9413, 5.0169997, 39.7973, -3.9413, 5.0169997, 42.6568, -3.9413, 3.3446, 45.5107, -3.9413, 5.0169997, 42.6568, -3.9413, 5.0169997, 39.7973, -3.9413, 3.3446, 42.6568, -3.9413, 3.3446, 42.6568, -3.9413, 5.0169997, 42.6568, -3.9413, 3.3446, 45.5107, -3.9413, 3.3446, 45.5107, -3.9413, 5.0169997, 39.7973, -3.9413, 1.6723, 42.6568, -3.9413, 3.3446, 39.7973, -3.9413, 3.3446, 42.6568, -3.9413, 1.6723, 45.5107, -3.9413, 3.3446, 42.6568, -3.9413, 3.3446, 39.7973, -3.9413, 1.6723, 42.6568, -3.9413, 1.6723, 42.6568, -3.9413, 3.3446, 42.6568, -3.9413, 1.6723, 45.5107, -3.9413, 1.6723, 45.5107, -3.9413, 3.3446, 39.7973, -3.9413, 0, 42.6568, -3.9413, 1.6723, 39.7973, -3.9413, 1.6723, 42.6568, -3.9413, 0, 45.5107, -3.9413, 1.6723, 42.6568, -3.9413, 1.6723, 39.7973, -3.9413, 0, 42.6568, -3.9413, 0, 42.6568, -3.9413, 1.6723, 42.6568, -3.9413, 0, 45.5107, -3.9413, 0, 45.5107, -3.9413, 1.6723, 39.7973, -3.9413, -1.6723, 42.6568, -3.9413, 0, 39.7973, -3.9413, 0, 42.6568, -3.9413, -1.6723, 45.5107, -3.9413, 0, 42.6568, -3.9413, 0, 39.7973, -3.9413, -1.6723, 42.6568, -3.9413, -1.6723, 42.6568, -3.9413, 0, 42.6568, -3.9413, -1.6723, 45.5107, -3.9413, -1.6723, 45.5107, -3.9413, 0, 39.7973, -3.9413, -3.3446, 42.6568, -3.9413, -1.6723, 39.7973, -3.9413, -1.6723, 42.6568, -3.9413, -3.3446, 45.5107, -3.9413, -1.6723, 42.6568, -3.9413, -1.6723, 39.7973, -3.9413, -3.3446, 42.6568, -3.9413, -3.3446, 42.6568, -3.9413, -1.6723, 42.6568, -3.9413, -3.3446, 45.5107, -3.9413, -3.3446, 45.5107, -3.9413, -1.6723, 39.7973, -3.9413, -5.0169997, 42.6568, -3.9413, -3.3446, 39.7973, -3.9413, -3.3446, 42.6568, -3.9413, -5.0169997, 45.5107, -3.9413, -3.3446, 42.6568, -3.9413, -3.3446, 39.7973, -3.9413, -5.0169997, 42.6568, -3.9413, -5.0169997, 42.6568, -3.9413, -3.3446, 42.6568, -3.9413, -5.0169997, 45.5107, -3.9413, -5.0169997, 45.5107, -3.9413, -3.3446, 39.7973, -3.9413, -6.6893, 42.6568, -3.9413, -5.0169997, 39.7973, -3.9413, -5.0169997, 42.6568, -3.9413, -6.6893, 45.5107, -3.9413, -5.0169997, 42.6568, -3.9413, -5.0169997, 39.7973, -3.9413, -6.6893, 42.6568, -3.9413, -6.6893, 42.6568, -3.9413, -5.0169997, 42.6568, -3.9413, -6.6893, 45.5107, -3.9413, -6.6893, 45.5107, -3.9413, -5.0169997, 39.7973, -3.9413, -8.3616, 42.6568, -3.9413, -6.6893, 39.7973, -3.9413, -6.6893, 42.6568, -3.9413, -8.3616, 45.5107, -3.9413, -6.6893, 42.6568, -3.9413, -6.6893, 39.7973, -3.9413, -8.3616, 42.6568, -3.9413, -8.3616, 42.6568, -3.9413, -6.6893, 42.6568, -3.9413, -8.3616, 45.5107, -3.9413, -8.3616, 45.5107, -3.9413, -6.6893, 39.7973, -3.9413, -10.033899, 42.6568, -3.9413, -8.3616, 39.7973, -3.9413, -8.3616, 42.6568, -3.9413, -10.033899, 45.5107, -3.9413, -8.3616, 42.6568, -3.9413, -8.3616, 39.7973, -3.9413, -10.033899, 42.6568, -3.9413, -10.033899, 42.6568, -3.9413, -8.3616, 42.6568, -3.9413, -10.033899, 45.5107, -3.9413, -10.033899, 45.5107, -3.9413, -8.3616, 39.7973, -3.9413, -11.7063, 42.6568, -3.9413, -10.033899, 39.7973, -3.9413, -10.033899, 42.6568, -3.9413, -11.7063, 45.5107, -3.9413, -10.033899, 42.6568, -3.9413, -10.033899, 39.7973, -3.9413, -11.7063, 42.6568, -3.9413, -11.7063, 42.6568, -3.9413, -10.033899, 42.6568, -3.9413, -11.7063, 45.5107, -3.9413, -11.7063, 45.5107, -3.9413, -10.033899, 39.7973, -3.9413, -13.3786, 42.6568, -3.9413, -11.7063, 39.7973, -3.9413, -11.7063, 42.6568, -3.9413, -13.3786, 45.5107, -3.9413, -11.7063, 42.6568, -3.9413, -11.7063, 39.7973, -3.9413, -13.3786, 42.6568, -3.9413, -13.3786, 42.6568, -3.9413, -11.7063, 42.6568, -3.9413, -13.3786, 45.5107, -3.9413, -13.3786, 45.5107, -3.9413, -11.7063, 39.7973, -3.9413, -15.0508995, 42.6568, -3.9413, -13.3786, 39.7973, -3.9413, -13.3786, 39.7973, -3.9413, -15.0508995, 42.6568, -3.9413, -15.0508995, 42.6568, -3.9413, -13.3786, 42.6568, -3.9413, -15.0508995, 45.5107, -3.9413, -13.3786, 42.6568, -3.9413, -13.3786, 39.7973, -3.9413, -16.7232, 42.6568, -3.9413, -15.0508995, 39.7973, -3.9413, -15.0508995, 42.6568, -3.9413, -15.0508995, 45.5107, -3.9413, -15.0508995, 45.5107, -3.9413, -13.3786, 45.5107, -3.9413, -15.0508995, 48.3645, -3.9413, -13.3786, 45.5107, -3.9413, -13.3786, 39.7973, -3.9413, -16.7232, 42.6568, -3.9413, -16.7232, 42.6568, -3.9413, -15.0508995, 42.6568, -3.9413, -16.7232, 45.5107, -3.9413, -15.0508995, 42.6568, -3.9413, -15.0508995, 39.7973, -3.9413, -18.3955, 42.6568, -3.9413, -16.7232, 39.7973, -3.9413, -16.7232, 45.5107, -3.9413, -15.0508995, 48.3645, -3.9413, -15.0508995, 48.3645, -3.9413, -13.3786, 39.7973, -3.9413, -18.3955, 42.6568, -3.9413, -18.3955, 42.6568, -3.9413, -16.7232, 42.6568, -3.9413, -16.7232, 45.5107, -3.9413, -16.7232, 45.5107, -3.9413, -15.0508995, 45.5107, -3.9413, -16.7232, 48.3645, -3.9413, -15.0508995, 45.5107, -3.9413, -15.0508995, 42.6568, -3.9413, -18.3955, 45.5107, -3.9413, -16.7232, 42.6568, -3.9413, -16.7232, 39.7973, -3.9413, -20.0679, 42.6568, -3.9413, -18.3955, 39.7973, -3.9413, -18.3955, 45.5107, -3.9413, -16.7232, 48.3645, -3.9413, -16.7232, 48.3645, -3.9413, -15.0508995, 48.3645, -3.9413, -16.7232, 51.2184, -3.9413, -15.0508995, 48.3645, -3.9413, -15.0508995, 42.6568, -3.9413, -18.3955, 45.5107, -3.9413, -18.3955, 45.5107, -3.9413, -16.7232, 45.5107, -3.9413, -18.3955, 48.3645, -3.9413, -16.7232, 45.5107, -3.9413, -16.7232, 39.7973, -3.9413, -20.0679, 42.6568, -3.9413, -20.0679, 42.6568, -3.9413, -18.3955, 42.6568, -3.9413, -20.0679, 45.5107, -3.9413, -18.3955, 42.6568, -3.9413, -18.3955, 39.7973, -3.9413, -21.7402, 42.6568, -3.9413, -20.0679, 39.7973, -3.9413, -20.0679, 48.3645, -3.9413, -16.7232, 51.2184, -3.9413, -16.7232, 51.2184, -3.9413, -15.0508995, 51.2184, -3.9413, -16.7232, 54.072197, -3.9413, -15.0508995, 51.2184, -3.9413, -15.0508995, 51.2184, -3.9413, -16.7232, 54.072197, -3.9413, -16.7232, 54.072197, -3.9413, -15.0508995, 48.3645, -3.9413, -18.3955, 51.2184, -3.9413, -16.7232, 48.3645, -3.9413, -16.7232, 45.5107, -3.9413, -18.3955, 48.3645, -3.9413, -18.3955, 48.3645, -3.9413, -16.7232, 51.2184, -3.9413, -18.3955, 54.072197, -3.9413, -16.7232, 51.2184, -3.9413, -16.7232, 48.3645, -3.9413, -18.3955, 51.2184, -3.9413, -18.3955, 51.2184, -3.9413, -16.7232, 51.2184, -3.9413, -18.3955, 54.072197, -3.9413, -18.3955, 54.072197, -3.9413, -16.7232, 45.5107, -3.9413, -20.0679, 48.3645, -3.9413, -18.3955, 45.5107, -3.9413, -18.3955, 42.6568, -3.9413, -20.0679, 45.5107, -3.9413, -20.0679, 45.5107, -3.9413, -18.3955, 48.3645, -3.9413, -20.0679, 51.2184, -3.9413, -18.3955, 48.3645, -3.9413, -18.3955, 45.5107, -3.9413, -20.0679, 48.3645, -3.9413, -20.0679, 48.3645, -3.9413, -18.3955, 51.2184, -3.9413, -20.0679, 54.072197, -3.9413, -18.3955, 51.2184, -3.9413, -18.3955, 48.3645, -3.9413, -20.0679, 51.2184, -3.9413, -20.0679, 51.2184, -3.9413, -18.3955, 51.2184, -3.9413, -20.0679, 54.072197, -3.9413, -20.0679, 54.072197, -3.9413, -18.3955, 39.7973, -3.9413, -21.7402, 42.6568, -3.9413, -21.7402, 42.6568, -3.9413, -20.0679, 42.6568, -3.9413, -21.7402, 45.5107, -3.9413, -20.0679, 42.6568, -3.9413, -20.0679, 39.7973, -3.9413, -23.412498, 42.6568, -3.9413, -21.7402, 39.7973, -3.9413, -21.7402, 45.5107, -3.9413, -21.7402, 48.3645, -3.9413, -20.0679, 45.5107, -3.9413, -20.0679, 42.6568, -3.9413, -21.7402, 45.5107, -3.9413, -21.7402, 45.5107, -3.9413, -20.0679, 39.7973, -3.9413, -23.412498, 42.6568, -3.9413, -23.412498, 42.6568, -3.9413, -21.7402, 42.6568, -3.9413, -23.412498, 45.5107, -3.9413, -21.7402, 42.6568, -3.9413, -21.7402, 39.7973, -3.9413, -25.084799, 42.6568, -3.9413, -23.412498, 39.7973, -3.9413, -23.412498, 45.5107, -3.9413, -21.7402, 48.3645, -3.9413, -21.7402, 48.3645, -3.9413, -20.0679, 48.3645, -3.9413, -21.7402, 51.2184, -3.9413, -20.0679, 48.3645, -3.9413, -20.0679, 42.6568, -3.9413, -23.412498, 45.5107, -3.9413, -23.412498, 45.5107, -3.9413, -21.7402, 45.5107, -3.9413, -23.412498, 48.3645, -3.9413, -21.7402, 45.5107, -3.9413, -21.7402, 39.7973, -3.9413, -25.084799, 42.6568, -3.9413, -25.084799, 42.6568, -3.9413, -23.412498, 42.6568, -3.9413, -25.084799, 45.5107, -3.9413, -23.412498, 42.6568, -3.9413, -23.412498, 39.7973, -3.9413, -26.7572, 42.6568, -3.9413, -25.084799, 39.7973, -3.9413, -25.084799, 39.7973, -3.9413, -26.7572, 42.6568, -3.9413, -26.7572, 42.6568, -3.9413, -25.084799, 42.6568, -3.9413, -26.7572, 45.5107, -3.9413, -25.084799, 42.6568, -3.9413, -25.084799, 42.6568, -3.9413, -25.084799, 45.5107, -3.9413, -25.084799, 45.5107, -3.9413, -23.412498, 42.6568, -3.9413, -26.7572, 45.5107, -3.9413, -26.7572, 45.5107, -3.9413, -25.084799, 45.5107, -3.9413, -23.412498, 48.3645, -3.9413, -23.412498, 48.3645, -3.9413, -21.7402, 45.5107, -3.9413, -25.084799, 48.3645, -3.9413, -23.412498, 45.5107, -3.9413, -23.412498, 45.5107, -3.9413, -26.7572, 48.3645, -3.9413, -25.084799, 45.5107, -3.9413, -25.084799, 45.5107, -3.9413, -25.084799, 48.3645, -3.9413, -25.084799, 48.3645, -3.9413, -23.412498, 45.5107, -3.9413, -26.7572, 48.3645, -3.9413, -26.7572, 48.3645, -3.9413, -25.084799, 48.3645, -3.9413, -21.7402, 51.2184, -3.9413, -21.7402, 51.2184, -3.9413, -20.0679, 48.3645, -3.9413, -23.412498, 51.2184, -3.9413, -21.7402, 48.3645, -3.9413, -21.7402, 51.2184, -3.9413, -21.7402, 54.072197, -3.9413, -20.0679, 51.2184, -3.9413, -20.0679, 51.2184, -3.9413, -21.7402, 54.072197, -3.9413, -21.7402, 54.072197, -3.9413, -20.0679, 48.3645, -3.9413, -23.412498, 51.2184, -3.9413, -23.412498, 51.2184, -3.9413, -21.7402, 51.2184, -3.9413, -23.412498, 54.072197, -3.9413, -21.7402, 51.2184, -3.9413, -21.7402, 48.3645, -3.9413, -25.084799, 51.2184, -3.9413, -23.412498, 48.3645, -3.9413, -23.412498, 51.2184, -3.9413, -23.412498, 54.072197, -3.9413, -23.412498, 54.072197, -3.9413, -21.7402, 48.3645, -3.9413, -26.7572, 51.2184, -3.9413, -25.084799, 48.3645, -3.9413, -25.084799, 48.3645, -3.9413, -25.084799, 51.2184, -3.9413, -25.084799, 51.2184, -3.9413, -23.412498, 51.2184, -3.9413, -25.084799, 54.072197, -3.9413, -23.412498, 51.2184, -3.9413, -23.412498, 48.3645, -3.9413, -26.7572, 51.2184, -3.9413, -26.7572, 51.2184, -3.9413, -25.084799, 51.2184, -3.9413, -25.084799, 54.072197, -3.9413, -25.084799, 54.072197, -3.9413, -23.412498, 51.2184, -3.9413, -26.7572, 54.072197, -3.9413, -25.084799, 51.2184, -3.9413, -25.084799, 51.2184, -3.9413, -26.7572, 54.072197, -3.9413, -26.7572, 54.072197, -3.9413, -25.084799, 29.679499, -0.6044, -15.0508995, 32.4649, -1.1213, -13.3786, 29.679499, -0.6044, -13.3786, 29.679499, -0.6044, -13.3786, 32.4649, -1.1213, -13.3786, 32.4649, -1.1213, -11.7063, 29.679499, -0.6044, -15.0508995, 32.4649, -1.1213, -15.0508995, 32.4649, -1.1213, -13.3786, 29.679499, -0.6044, -13.3786, 32.4649, -1.1213, -11.7063, 29.679499, -0.6044, -11.7063, 29.679499, -0.6044, -16.7232, 32.4649, -1.1213, -15.0508995, 29.679499, -0.6044, -15.0508995, 29.679499, -0.6044, -11.7063, 32.4649, -1.1213, -11.7063, 32.4649, -1.1213, -10.033899, 29.679499, -0.6044, -16.7232, 32.4649, -1.1213, -16.7232, 32.4649, -1.1213, -15.0508995, 29.679499, -0.6044, -11.7063, 32.4649, -1.1213, -10.033899, 29.679499, -0.6044, -10.033899, 29.679499, -0.6044, -18.3955, 32.4649, -1.1213, -16.7232, 29.679499, -0.6044, -16.7232, 29.679499, -0.6044, -10.033899, 32.4649, -1.1213, -10.033899, 32.4649, -1.1213, -8.3616, 29.679499, -0.6044, -18.3955, 32.4649, -1.1213, -18.3955, 32.4649, -1.1213, -16.7232, 29.679499, -0.6044, -10.033899, 32.4649, -1.1213, -8.3616, 29.679499, -0.6044, -8.3616, 29.679499, -0.6044, -20.0679, 32.4649, -1.1213, -18.3955, 29.679499, -0.6044, -18.3955, 29.679499, -0.6044, -8.3616, 32.4649, -1.1213, -8.3616, 32.4649, -1.1213, -6.6893, 29.679499, -0.6044, -20.0679, 32.4649, -1.1213, -20.0679, 32.4649, -1.1213, -18.3955, 29.679499, -0.6044, -8.3616, 32.4649, -1.1213, -6.6893, 29.679499, -0.6044, -6.6893, 29.679499, -0.6044, -21.7402, 32.4649, -1.1213, -20.0679, 29.679499, -0.6044, -20.0679, 29.679499, -0.6044, -6.6893, 32.4649, -1.1213, -6.6893, 32.4649, -1.1213, -5.0169997, 29.679499, -0.6044, -21.7402, 32.4649, -1.1213, -21.7402, 32.4649, -1.1213, -20.0679, 29.679499, -0.6044, -6.6893, 32.4649, -1.1213, -5.0169997, 29.679499, -0.6044, -5.0169997, 29.679499, -0.6044, -23.412498, 32.4649, -1.1213, -21.7402, 29.679499, -0.6044, -21.7402, 29.679499, -0.6044, -5.0169997, 32.4649, -1.1213, -5.0169997, 32.4649, -1.1213, -3.3446, 29.679499, -0.6044, -23.412498, 32.4649, -1.1213, -23.412498, 32.4649, -1.1213, -21.7402, 29.679499, -0.6044, -5.0169997, 32.4649, -1.1213, -3.3446, 29.679499, -0.6044, -3.3446, 29.679499, -0.6044, -25.084799, 32.4649, -1.1213, -23.412498, 29.679499, -0.6044, -23.412498, 29.679499, -0.6044, -3.3446, 32.4649, -1.1213, -3.3446, 32.4649, -1.1213, -1.6723, 29.679499, -0.6044, -25.084799, 32.4649, -1.1213, -25.084799, 32.4649, -1.1213, -23.412498, 29.679499, -0.6044, -26.7572, 32.4649, -1.1213, -25.084799, 29.679499, -0.6044, -25.084799, 29.679499, -0.6044, -26.7572, 32.4649, -1.1213, -26.7572, 32.4649, -1.1213, -25.084799, 29.679499, -0.6044, -3.3446, 32.4649, -1.1213, -1.6723, 29.679499, -0.6044, -1.6723, 29.679499, -0.6044, -1.6723, 32.4649, -1.1213, -1.6723, 32.4649, -1.1213, 0, 29.679499, -0.6044, -1.6723, 32.4649, -1.1213, 0, 29.679499, -0.6044, 0, 29.679499, -0.6044, 0, 32.4649, -1.1213, 0, 32.4649, -1.1213, 1.6723, 29.679499, -0.6044, 0, 32.4649, -1.1213, 1.6723, 29.679499, -0.6044, 1.6723, 29.679499, -0.6044, 1.6723, 32.4649, -1.1213, 1.6723, 32.4649, -1.1213, 3.3446, 29.679499, -0.6044, 1.6723, 32.4649, -1.1213, 3.3446, 29.679499, -0.6044, 3.3446, 29.679499, -0.6044, 3.3446, 32.4649, -1.1213, 3.3446, 32.4649, -1.1213, 5.0169997, 29.679499, -0.6044, 3.3446, 32.4649, -1.1213, 5.0169997, 29.679499, -0.6044, 5.0169997, 29.679499, -0.6044, 5.0169997, 32.4649, -1.1213, 5.0169997, 32.4649, -1.1213, 6.6893, 29.679499, -0.6044, 5.0169997, 32.4649, -1.1213, 6.6893, 29.679499, -0.6044, 6.6893, 29.679499, -0.6044, 6.6893, 32.4649, -1.1213, 6.6893, 32.4649, -1.1213, 8.3616, 29.679499, -0.6044, 6.6893, 32.4649, -1.1213, 8.3616, 29.679499, -0.6044, 8.3616, 29.679499, -0.6044, 8.3616, 32.4649, -1.1213, 8.3616, 32.4649, -1.1213, 10.033899, 29.679499, -0.6044, 8.3616, 32.4649, -1.1213, 10.033899, 29.679499, -0.6044, 10.033899, 29.679499, -0.6044, 10.033899, 32.4649, -1.1213, 10.033899, 32.4649, -1.1213, 11.7063, 29.679499, -0.6044, 10.033899, 32.4649, -1.1213, 11.7063, 29.679499, -0.6044, 11.7063, 29.679499, -0.6044, 11.7063, 32.4649, -1.1213, 11.7063, 32.4649, -1.1213, 13.3786, 29.679499, -0.6044, 11.7063, 32.4649, -1.1213, 13.3786, 29.679499, -0.6044, 13.3786, 29.679499, -0.6044, 13.3786, 32.4649, -1.1213, 13.3786, 32.4649, -1.1213, 15.0508995, 29.679499, -0.6044, 13.3786, 32.4649, -1.1213, 15.0508995, 29.679499, -0.6044, 15.0508995, 29.679499, -0.6044, 15.0508995, 32.4649, -1.1213, 15.0508995, 32.4649, -1.1213, 16.7232, 29.679499, -0.6044, 15.0508995, 32.4649, -1.1213, 16.7232, 29.679499, -0.6044, 16.7232, 29.679499, -0.6044, 16.7232, 32.4649, -1.1213, 16.7232, 32.4649, -1.1213, 18.3955, 29.679499, -0.6044, 16.7232, 32.4649, -1.1213, 18.3955, 29.679499, -0.6044, 18.3955, 29.679499, -0.6044, 18.3955, 32.4649, -1.1213, 18.3955, 32.4649, -1.1213, 20.0679, 29.679499, -0.6044, 18.3955, 32.4649, -1.1213, 20.0679, 29.679499, -0.6044, 20.0679, 29.679499, -0.6044, 20.0679, 32.4649, -1.1213, 20.0679, 32.4649, -1.1213, 21.7402, 29.679499, -0.6044, 20.0679, 32.4649, -1.1213, 21.7402, 29.679499, -0.6044, 21.7402, 29.679499, -0.6044, 21.7402, 32.4649, -1.1213, 21.7402, 32.4649, -1.1213, 23.412498, 29.679499, -0.6044, 21.7402, 32.4649, -1.1213, 23.412498, 29.679499, -0.6044, 23.412498, 29.679499, -0.6044, 23.412498, 32.4649, -1.1213, 23.412498, 32.4649, -1.1213, 25.084799, 29.679499, -0.6044, 23.412498, 32.4649, -1.1213, 25.084799, 29.679499, -0.6044, 25.084799, 29.679499, -0.6044, 25.084799, 32.4649, -1.1213, 25.084799, 32.4649, -1.1213, 26.7572, 29.679499, -0.6044, 25.084799, 32.4649, -1.1213, 26.7572, 29.679499, -0.6044, 26.7572, 32.4649, -1.1213, -15.0508995, 39.7973, -3.9413, -13.3786, 32.4649, -1.1213, -13.3786, 32.4649, -1.1213, -13.3786, 39.7973, -3.9413, -13.3786, 39.7973, -3.9413, -11.7063, 32.4649, -1.1213, -15.0508995, 39.7973, -3.9413, -15.0508995, 39.7973, -3.9413, -13.3786, 32.4649, -1.1213, -13.3786, 39.7973, -3.9413, -11.7063, 32.4649, -1.1213, -11.7063, 32.4649, -1.1213, -16.7232, 39.7973, -3.9413, -15.0508995, 32.4649, -1.1213, -15.0508995, 32.4649, -1.1213, -11.7063, 39.7973, -3.9413, -11.7063, 39.7973, -3.9413, -10.033899, 32.4649, -1.1213, -16.7232, 39.7973, -3.9413, -16.7232, 39.7973, -3.9413, -15.0508995, 32.4649, -1.1213, -11.7063, 39.7973, -3.9413, -10.033899, 32.4649, -1.1213, -10.033899, 32.4649, -1.1213, -18.3955, 39.7973, -3.9413, -16.7232, 32.4649, -1.1213, -16.7232, 32.4649, -1.1213, -10.033899, 39.7973, -3.9413, -10.033899, 39.7973, -3.9413, -8.3616, 32.4649, -1.1213, -18.3955, 39.7973, -3.9413, -18.3955, 39.7973, -3.9413, -16.7232, 32.4649, -1.1213, -10.033899, 39.7973, -3.9413, -8.3616, 32.4649, -1.1213, -8.3616, 32.4649, -1.1213, -20.0679, 39.7973, -3.9413, -18.3955, 32.4649, -1.1213, -18.3955, 32.4649, -1.1213, -8.3616, 39.7973, -3.9413, -8.3616, 39.7973, -3.9413, -6.6893, 32.4649, -1.1213, -20.0679, 39.7973, -3.9413, -20.0679, 39.7973, -3.9413, -18.3955, 32.4649, -1.1213, -8.3616, 39.7973, -3.9413, -6.6893, 32.4649, -1.1213, -6.6893, 32.4649, -1.1213, -21.7402, 39.7973, -3.9413, -20.0679, 32.4649, -1.1213, -20.0679, 32.4649, -1.1213, -6.6893, 39.7973, -3.9413, -6.6893, 39.7973, -3.9413, -5.0169997, 32.4649, -1.1213, -21.7402, 39.7973, -3.9413, -21.7402, 39.7973, -3.9413, -20.0679, 32.4649, -1.1213, -6.6893, 39.7973, -3.9413, -5.0169997, 32.4649, -1.1213, -5.0169997, 32.4649, -1.1213, -23.412498, 39.7973, -3.9413, -21.7402, 32.4649, -1.1213, -21.7402, 32.4649, -1.1213, -5.0169997, 39.7973, -3.9413, -5.0169997, 39.7973, -3.9413, -3.3446, 32.4649, -1.1213, -23.412498, 39.7973, -3.9413, -23.412498, 39.7973, -3.9413, -21.7402, 32.4649, -1.1213, -5.0169997, 39.7973, -3.9413, -3.3446, 32.4649, -1.1213, -3.3446, 32.4649, -1.1213, -25.084799, 39.7973, -3.9413, -23.412498, 32.4649, -1.1213, -23.412498, 32.4649, -1.1213, -3.3446, 39.7973, -3.9413, -3.3446, 39.7973, -3.9413, -1.6723, 32.4649, -1.1213, -25.084799, 39.7973, -3.9413, -25.084799, 39.7973, -3.9413, -23.412498, 32.4649, -1.1213, -26.7572, 39.7973, -3.9413, -25.084799, 32.4649, -1.1213, -25.084799, 32.4649, -1.1213, -26.7572, 39.7973, -3.9413, -26.7572, 39.7973, -3.9413, -25.084799, 32.4649, -1.1213, -3.3446, 39.7973, -3.9413, -1.6723, 32.4649, -1.1213, -1.6723, 32.4649, -1.1213, -1.6723, 39.7973, -3.9413, -1.6723, 39.7973, -3.9413, 0, 32.4649, -1.1213, -1.6723, 39.7973, -3.9413, 0, 32.4649, -1.1213, 0, 32.4649, -1.1213, 0, 39.7973, -3.9413, 0, 39.7973, -3.9413, 1.6723, 32.4649, -1.1213, 0, 39.7973, -3.9413, 1.6723, 32.4649, -1.1213, 1.6723, 32.4649, -1.1213, 1.6723, 39.7973, -3.9413, 1.6723, 39.7973, -3.9413, 3.3446, 32.4649, -1.1213, 1.6723, 39.7973, -3.9413, 3.3446, 32.4649, -1.1213, 3.3446, 32.4649, -1.1213, 3.3446, 39.7973, -3.9413, 3.3446, 39.7973, -3.9413, 5.0169997, 32.4649, -1.1213, 3.3446, 39.7973, -3.9413, 5.0169997, 32.4649, -1.1213, 5.0169997, 32.4649, -1.1213, 5.0169997, 39.7973, -3.9413, 5.0169997, 39.7973, -3.9413, 6.6893, 32.4649, -1.1213, 5.0169997, 39.7973, -3.9413, 6.6893, 32.4649, -1.1213, 6.6893, 32.4649, -1.1213, 6.6893, 39.7973, -3.9413, 6.6893, 39.7973, -3.9413, 8.3616, 32.4649, -1.1213, 6.6893, 39.7973, -3.9413, 8.3616, 32.4649, -1.1213, 8.3616, 32.4649, -1.1213, 8.3616, 39.7973, -3.9413, 8.3616, 39.7973, -3.9413, 10.033899, 32.4649, -1.1213, 8.3616, 39.7973, -3.9413, 10.033899, 32.4649, -1.1213, 10.033899, 32.4649, -1.1213, 10.033899, 39.7973, -3.9413, 10.033899, 39.7973, -3.9413, 11.7063, 32.4649, -1.1213, 10.033899, 39.7973, -3.9413, 11.7063, 32.4649, -1.1213, 11.7063, 32.4649, -1.1213, 11.7063, 39.7973, -3.9413, 11.7063, 39.7973, -3.9413, 13.3786, 32.4649, -1.1213, 11.7063, 39.7973, -3.9413, 13.3786, 32.4649, -1.1213, 13.3786, 32.4649, -1.1213, 13.3786, 39.7973, -3.9413, 13.3786, 39.7973, -3.9413, 15.0508995, 32.4649, -1.1213, 13.3786, 39.7973, -3.9413, 15.0508995, 32.4649, -1.1213, 15.0508995, 32.4649, -1.1213, 15.0508995, 39.7973, -3.9413, 15.0508995, 39.7973, -3.9413, 16.7232, 32.4649, -1.1213, 15.0508995, 39.7973, -3.9413, 16.7232, 32.4649, -1.1213, 16.7232, 32.4649, -1.1213, 16.7232, 39.7973, -3.9413, 16.7232, 39.7973, -3.9413, 18.3955, 32.4649, -1.1213, 16.7232, 39.7973, -3.9413, 18.3955, 32.4649, -1.1213, 18.3955, 32.4649, -1.1213, 18.3955, 39.7973, -3.9413, 18.3955, 39.7973, -3.9413, 20.0679, 32.4649, -1.1213, 18.3955, 39.7973, -3.9413, 20.0679, 32.4649, -1.1213, 20.0679, 32.4649, -1.1213, 20.0679, 39.7973, -3.9413, 20.0679, 39.7973, -3.9413, 21.7402, 32.4649, -1.1213, 20.0679, 39.7973, -3.9413, 21.7402, 32.4649, -1.1213, 21.7402, 32.4649, -1.1213, 21.7402, 39.7973, -3.9413, 21.7402, 39.7973, -3.9413, 23.412498, 32.4649, -1.1213, 21.7402, 39.7973, -3.9413, 23.412498, 32.4649, -1.1213, 23.412498, 32.4649, -1.1213, 23.412498, 39.7973, -3.9413, 23.412498, 39.7973, -3.9413, 25.084799, 32.4649, -1.1213, 23.412498, 39.7973, -3.9413, 25.084799, 32.4649, -1.1213, 25.084799, 32.4649, -1.1213, 25.084799, 39.7973, -3.9413, 25.084799, 39.7973, -3.9413, 26.7572, 32.4649, -1.1213, 25.084799, 39.7973, -3.9413, 26.7572, 32.4649, -1.1213, 26.7572, 26.7572, -0.2593, 16.7232, 29.679499, -0.6044, 18.3955, 26.7572, -0.2593, 18.3955, 26.7572, -0.2593, 18.3955, 29.679499, -0.6044, 18.3955, 29.679499, -0.6044, 20.0679, 26.7572, -0.2593, 16.7232, 29.679499, -0.6044, 16.7232, 29.679499, -0.6044, 18.3955, 26.7572, -0.2593, 18.3955, 29.679499, -0.6044, 20.0679, 26.7572, -0.2593, 20.0679, 26.7572, -0.2593, 15.0508995, 29.679499, -0.6044, 16.7232, 26.7572, -0.2593, 16.7232, 26.7572, -0.2593, 20.0679, 29.679499, -0.6044, 20.0679, 29.679499, -0.6044, 21.7402, 26.7572, -0.2593, 15.0508995, 29.679499, -0.6044, 15.0508995, 29.679499, -0.6044, 16.7232, 26.7572, -0.2593, 20.0679, 29.679499, -0.6044, 21.7402, 26.7572, -0.2593, 21.7402, 26.7572, -0.2593, 13.3786, 29.679499, -0.6044, 15.0508995, 26.7572, -0.2593, 15.0508995, 26.7572, -0.2593, 21.7402, 29.679499, -0.6044, 21.7402, 29.679499, -0.6044, 23.412498, 26.7572, -0.2593, 13.3786, 29.679499, -0.6044, 13.3786, 29.679499, -0.6044, 15.0508995, 26.7572, -0.2593, 21.7402, 29.679499, -0.6044, 23.412498, 26.7572, -0.2593, 23.412498, 26.7572, -0.2593, 11.7063, 29.679499, -0.6044, 13.3786, 26.7572, -0.2593, 13.3786, 26.7572, -0.2593, 23.412498, 29.679499, -0.6044, 23.412498, 29.679499, -0.6044, 25.084799, 26.7572, -0.2593, 11.7063, 29.679499, -0.6044, 11.7063, 29.679499, -0.6044, 13.3786, 26.7572, -0.2593, 23.412498, 29.679499, -0.6044, 25.084799, 26.7572, -0.2593, 25.084799, 26.7572, -0.2593, 25.084799, 29.679499, -0.6044, 25.084799, 29.679499, -0.6044, 26.7572, 26.7572, -0.2593, 25.084799, 29.679499, -0.6044, 26.7572, 26.7572, -0.2593, 26.7572, 26.7572, -0.2593, 10.033899, 29.679499, -0.6044, 11.7063, 26.7572, -0.2593, 11.7063, 26.7572, -0.2593, 10.033899, 29.679499, -0.6044, 10.033899, 29.679499, -0.6044, 11.7063, 26.7572, -0.2593, 8.3616, 29.679499, -0.6044, 10.033899, 26.7572, -0.2593, 10.033899, 26.7572, -0.2593, 8.3616, 29.679499, -0.6044, 8.3616, 29.679499, -0.6044, 10.033899, 26.7572, -0.2593, 6.6893, 29.679499, -0.6044, 8.3616, 26.7572, -0.2593, 8.3616, 26.7572, -0.2593, 6.6893, 29.679499, -0.6044, 6.6893, 29.679499, -0.6044, 8.3616, 26.7572, -0.2593, 5.0169997, 29.679499, -0.6044, 6.6893, 26.7572, -0.2593, 6.6893, 26.7572, -0.2593, 5.0169997, 29.679499, -0.6044, 5.0169997, 29.679499, -0.6044, 6.6893, 26.7572, -0.2593, 3.3446, 29.679499, -0.6044, 5.0169997, 26.7572, -0.2593, 5.0169997, 26.7572, -0.2593, 3.3446, 29.679499, -0.6044, 3.3446, 29.679499, -0.6044, 5.0169997, 26.7572, -0.2593, 1.6723, 29.679499, -0.6044, 3.3446, 26.7572, -0.2593, 3.3446, 26.7572, -0.2593, 1.6723, 29.679499, -0.6044, 1.6723, 29.679499, -0.6044, 3.3446, 26.7572, -0.2593, 0, 29.679499, -0.6044, 1.6723, 26.7572, -0.2593, 1.6723, 26.7572, -0.2593, 0, 29.679499, -0.6044, 0, 29.679499, -0.6044, 1.6723, 26.7572, -0.2593, -1.6723, 29.679499, -0.6044, 0, 26.7572, -0.2593, 0, 26.7572, -0.2593, -1.6723, 29.679499, -0.6044, -1.6723, 29.679499, -0.6044, 0, 26.7572, -0.2593, -3.3446, 29.679499, -0.6044, -1.6723, 26.7572, -0.2593, -1.6723, 26.7572, -0.2593, -3.3446, 29.679499, -0.6044, -3.3446, 29.679499, -0.6044, -1.6723, 26.7572, -0.2593, -5.0169997, 29.679499, -0.6044, -3.3446, 26.7572, -0.2593, -3.3446, 26.7572, -0.2593, -5.0169997, 29.679499, -0.6044, -5.0169997, 29.679499, -0.6044, -3.3446, 26.7572, -0.2593, -6.6893, 29.679499, -0.6044, -5.0169997, 26.7572, -0.2593, -5.0169997, 26.7572, -0.2593, -6.6893, 29.679499, -0.6044, -6.6893, 29.679499, -0.6044, -5.0169997, 26.7572, -0.2593, -8.3616, 29.679499, -0.6044, -6.6893, 26.7572, -0.2593, -6.6893, 26.7572, -0.2593, -8.3616, 29.679499, -0.6044, -8.3616, 29.679499, -0.6044, -6.6893, 26.7572, -0.2593, -10.033899, 29.679499, -0.6044, -8.3616, 26.7572, -0.2593, -8.3616, 26.7572, -0.2593, -10.033899, 29.679499, -0.6044, -10.033899, 29.679499, -0.6044, -8.3616, 26.7572, -0.2593, -11.7063, 29.679499, -0.6044, -10.033899, 26.7572, -0.2593, -10.033899, 26.7572, -0.2593, -11.7063, 29.679499, -0.6044, -11.7063, 29.679499, -0.6044, -10.033899, 26.7572, -0.2593, -13.3786, 29.679499, -0.6044, -11.7063, 26.7572, -0.2593, -11.7063, 26.7572, -0.2593, -13.3786, 29.679499, -0.6044, -13.3786, 29.679499, -0.6044, -11.7063, 26.7572, -0.2593, -15.0508995, 29.679499, -0.6044, -13.3786, 26.7572, -0.2593, -13.3786, 26.7572, -0.2593, -15.0508995, 29.679499, -0.6044, -15.0508995, 29.679499, -0.6044, -13.3786, 26.7572, -0.2593, -16.7232, 29.679499, -0.6044, -15.0508995, 26.7572, -0.2593, -15.0508995, 26.7572, -0.2593, -16.7232, 29.679499, -0.6044, -16.7232, 29.679499, -0.6044, -15.0508995, 26.7572, -0.2593, -18.3955, 29.679499, -0.6044, -16.7232, 26.7572, -0.2593, -16.7232, 26.7572, -0.2593, -18.3955, 29.679499, -0.6044, -18.3955, 29.679499, -0.6044, -16.7232, 26.7572, -0.2593, -20.0679, 29.679499, -0.6044, -18.3955, 26.7572, -0.2593, -18.3955, 26.7572, -0.2593, -20.0679, 29.679499, -0.6044, -20.0679, 29.679499, -0.6044, -18.3955, 26.7572, -0.2593, -21.7402, 29.679499, -0.6044, -20.0679, 26.7572, -0.2593, -20.0679, 26.7572, -0.2593, -21.7402, 29.679499, -0.6044, -21.7402, 29.679499, -0.6044, -20.0679, 26.7572, -0.2593, -23.412498, 29.679499, -0.6044, -21.7402, 26.7572, -0.2593, -21.7402, 26.7572, -0.2593, -23.412498, 29.679499, -0.6044, -23.412498, 29.679499, -0.6044, -21.7402, 26.7572, -0.2593, -25.084799, 29.679499, -0.6044, -23.412498, 26.7572, -0.2593, -23.412498, 26.7572, -0.2593, -25.084799, 29.679499, -0.6044, -25.084799, 29.679499, -0.6044, -23.412498, 26.7572, -0.2593, -26.7572, 29.679499, -0.6044, -25.084799, 26.7572, -0.2593, -25.084799, 26.7572, -0.2593, -26.7572, 29.679499, -0.6044, -26.7572, 29.679499, -0.6044, -25.084799) @@ -163,6 +165,7 @@ location_tags = Array[StringName]([&"starter_pond", &"pond"]) selection_priority = 1 recovery_entry_height_reference = 1 recovery_entry_depth_threshold = 0.1 +recovery_entry_confirmation_seconds = 0.2 [node name="VisualWater" type="MeshInstance3D" parent="WaterBodies/Pond" unique_id=1034265960] material_override = ExtResource("19_fresh_water") @@ -287,3 +290,11 @@ transform = Transform3D(-0.9976046, 0, -0.06917416, 0, 1, 0, 0.06917416, 0, -0.9 [node name="PlayerStorageBox" parent="Interactables" instance=ExtResource("8_storage")] position = Vector3(-3.25, 3.42, 11.25) + +[node name="DecorShopWorld" parent="Interactables" instance=ExtResource("25_decor_shop")] +position = Vector3(-3.25, 3.42, 11.25) +rotation = Vector3(0, 1.5707964, 0) + +[node name="RVUpgradeShopWorld" parent="Interactables" instance=ExtResource("26_rv_upgrade")] +position = Vector3(-6.25, 3.42, 9) +rotation = Vector3(0, 1.5707964, 0) diff --git a/world/regions/world_region.gd b/world/regions/world_region.gd index a0730e4..d054938 100644 --- a/world/regions/world_region.gd +++ b/world/regions/world_region.gd @@ -18,6 +18,14 @@ func get_fishing_shop() -> FishingShopInteraction: return null +func get_decor_shop() -> DecorShopInteraction: + return null + + +func get_rv_upgrade_shop() -> RVUpgradeInteraction: + return null + + func get_player_storage() -> PlayerStorageInteraction: return null @@ -38,10 +46,24 @@ func get_spawn_surface_triangles( _material_names: Array[StringName], _minimum_global_y: float, _minimum_up_dot: float = 0.6, + _maximum_global_y: float = 100.0, ) -> Array[PackedVector3Array]: return [] +func get_saltwater_surface_height() -> float: + for water_region: FishableWaterRegion in get_fishable_water_regions(): + if water_region.water_type == WaterType.Type.SALT_WATER: + return water_region.get_surface_height() + return NAN + + +## Authored regions are assumed to expose only deliberately reachable home +## sites. Procedural regions override this to reject isolated elevations. +func is_home_exterior_position_accessible(_position: Vector3) -> bool: + return true + + func get_fishable_water_regions() -> Array[FishableWaterRegion]: var regions: Array[FishableWaterRegion] = [] var root: Node = get_node_or_null(fishable_water_root) @@ -72,6 +94,24 @@ func get_safe_respawn_points() -> Array[SafeRespawnPoint]: return points +func get_water_recovery_position( + entry_position: Vector3, + fallback_position: Vector3, +) -> Vector3: + if not entry_position.is_finite(): + return fallback_position + var target_position := fallback_position + var nearest_distance := INF + for point: SafeRespawnPoint in get_safe_respawn_points(): + if point == null or not is_instance_valid(point) or not point.enabled: + continue + var distance := point.get_horizontal_distance_squared(entry_position) + if distance < nearest_distance: + nearest_distance = distance + target_position = point.global_position + return target_position + + func get_diggable_areas() -> Array[DiggableArea3D]: var areas: Array[DiggableArea3D] = [] var root: Node = get_node_or_null(diggable_area_root) diff --git a/world/rv_upgrade_interaction.gd b/world/rv_upgrade_interaction.gd new file mode 100644 index 0000000..fb24246 --- /dev/null +++ b/world/rv_upgrade_interaction.gd @@ -0,0 +1,15 @@ +class_name RVUpgradeInteraction +extends FishingShopInteraction + +const TENT_DIALOGUE_TEXT: String = ( + "That tent is looking awful cramped. Can I talk you into an upgrade?" +) +const RV_DIALOGUE_TEXT: String = "How's the new RV treating you?" + + +func get_dialogue_text(home_tier: int = -1) -> String: + return ( + RV_DIALOGUE_TEXT + if home_tier >= PlayerHomeState.STARTER_RV_TIER + else TENT_DIALOGUE_TEXT + ) diff --git a/world/rv_upgrade_interaction.gd.uid b/world/rv_upgrade_interaction.gd.uid new file mode 100644 index 0000000..cb57804 --- /dev/null +++ b/world/rv_upgrade_interaction.gd.uid @@ -0,0 +1 @@ +uid://c0h08goorv2of diff --git a/world/test_world.gd b/world/test_world.gd index d5ac83c..2e25206 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -7,6 +7,12 @@ const FishingShopInteractionType = preload( const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const WorldLayoutType = preload("res://world/world_layout.gd") const GENERATED_REGION_SCENE: PackedScene = preload( "res://world/generation/generated_world_region.tscn" @@ -31,6 +37,7 @@ var _world_layout: StringName = WorldLayoutType.GENERATED var _world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED var _light_performance_profile: bool = false var _dedicated_simulation: bool = false +var _local_home_interior_presentation_active: bool = false func get_player_water_triggers() -> Array[PlayerWaterTrigger]: @@ -49,6 +56,16 @@ func get_safe_respawn_points() -> Array[SafeRespawnPoint]: ) +func get_water_recovery_position(entry_position: Vector3) -> Vector3: + if _active_region == null: + return global_position + var fallback_position := get_player_spawn_transform().origin + return _active_region.get_water_recovery_position( + entry_position, + fallback_position, + ) + + func get_fishing_shop() -> FishingShopInteractionType: return _active_region.get_fishing_shop() @@ -57,6 +74,14 @@ func get_player_storage() -> PlayerStorageInteractionType: return _active_region.get_player_storage() +func get_decor_shop() -> DecorShopInteractionType: + return _active_region.get_decor_shop() + + +func get_rv_upgrade_shop() -> RVUpgradeInteractionType: + return _active_region.get_rv_upgrade_shop() + + func get_player_spawn_transform() -> Transform3D: return _active_region.get_player_spawn_transform() @@ -75,6 +100,12 @@ func set_light_performance_profile(enabled: bool) -> void: _active_region.set_light_performance_profile(enabled) +func set_local_home_interior_presentation_active(enabled: bool) -> void: + _local_home_interior_presentation_active = enabled + if _active_region != null and not _dedicated_simulation: + _active_region.visible = not enabled + + func set_dedicated_simulation(enabled: bool) -> void: _dedicated_simulation = enabled if _active_region != null: @@ -146,10 +177,29 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D: func get_spawn_surface_triangles( material_names: Array[StringName], minimum_global_y: float, + minimum_up_dot: float = 0.6, + maximum_global_y: float = 100.0, ) -> Array[PackedVector3Array]: return _active_region.get_spawn_surface_triangles( material_names, minimum_global_y, + minimum_up_dot, + maximum_global_y, + ) + + +func get_saltwater_surface_height() -> float: + return ( + _active_region.get_saltwater_surface_height() + if _active_region != null + else NAN + ) + + +func is_home_exterior_position_accessible(position: Vector3) -> bool: + return ( + _active_region != null + and _active_region.is_home_exterior_position_accessible(position) ) @@ -207,6 +257,16 @@ func get_diggable_area_triangles( return [] +func is_diggable_surface_point_valid( + area_id: StringName, + point: Vector3, +) -> bool: + if _active_region == null: + return false + var area: DiggableArea3D = _active_region.get_diggable_area(area_id) + return area != null and area.is_surface_point_valid(point) + + func get_gatherable_spawn_positions( anchor_set_id: StringName, ) -> PackedVector3Array: @@ -252,6 +312,8 @@ func _replace_active_region(layout: StringName, seed: int) -> bool: _active_region.set_light_performance_profile(_light_performance_profile) if _dedicated_simulation: _set_region_presentation_enabled(_active_region, false) + else: + _active_region.visible = not _local_home_interior_presentation_active return true diff --git a/world/test_world.tscn b/world/test_world.tscn index 18697aa..2a94f53 100644 --- a/world/test_world.tscn +++ b/world/test_world.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://world/test_world.gd" id="1_world"] [ext_resource type="PackedScene" path="res://world/generation/generated_world_region.tscn" id="2_island"] [ext_resource type="Script" path="res://world/below_world_failsafe.gd" id="3_water_trigger"] -[ext_resource type="Environment" path="res://world/environment/netfishing_environment.tres" id="4_environment"] +[ext_resource type="Environment" path="res://world/environment/straywild_environment.tres" id="4_environment"] [sub_resource type="BoxShape3D" id="NorthSouthBoundsShape"] size = Vector3(168.8772, 14, 2) diff --git a/world/water_body_authoring.gd b/world/water_body_authoring.gd index a272d90..16636f7 100644 --- a/world/water_body_authoring.gd +++ b/world/water_body_authoring.gd @@ -80,6 +80,11 @@ var recovery_entry_depth_threshold: float = 0.35: set(value): recovery_entry_depth_threshold = clampf(value, 0.0, 2.0) _sync_owned_nodes() +@export_range(0.0, 1.0, 0.05) +var recovery_entry_confirmation_seconds: float = 0.0: + set(value): + recovery_entry_confirmation_seconds = clampf(value, 0.0, 1.0) + _sync_owned_nodes() @export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m") var recovery_depth: float = 4.8: set(value): @@ -173,6 +178,9 @@ func _sync_owned_nodes() -> void: recovery_region.entry_depth_threshold = ( recovery_entry_depth_threshold ) + recovery_region.entry_confirmation_seconds = ( + recovery_entry_confirmation_seconds + ) if uses_polygon and _sync_polygon_collision_shapes( recovery_region, recovery_shape_node, diff --git a/world/water_recovery_controller.gd b/world/water_recovery_controller.gd index 9626881..0d0c5aa 100644 --- a/world/water_recovery_controller.gd +++ b/world/water_recovery_controller.gd @@ -25,6 +25,7 @@ var _game_ui: GameUI var _screen_fade: ScreenFade var _water_triggers: Array[PlayerWaterTrigger] = [] var _safe_points: Array[SafeRespawnPoint] = [] +var _recovery_position_resolver := Callable() var _initial_spawn_transform: Transform3D var _entry_position: Vector3 var _bob_base_position: Vector3 @@ -43,6 +44,7 @@ func setup( screen_fade: ScreenFade, water_triggers: Array[PlayerWaterTrigger], safe_points: Array[SafeRespawnPoint], + recovery_position_resolver: Callable = Callable(), ) -> void: _player = player _fishing_spot = fishing_spot @@ -50,6 +52,7 @@ func setup( _screen_fade = screen_fade _water_triggers = water_triggers _safe_points = safe_points + _recovery_position_resolver = recovery_position_resolver _initial_spawn_transform = player.global_transform for water_trigger: PlayerWaterTrigger in _water_triggers: if ( @@ -73,6 +76,7 @@ func update_world_context( initial_spawn_transform: Transform3D, water_triggers: Array[PlayerWaterTrigger], safe_points: Array[SafeRespawnPoint], + recovery_position_resolver: Callable = Callable(), ) -> void: for water_trigger: PlayerWaterTrigger in _water_triggers: if ( @@ -88,6 +92,7 @@ func update_world_context( _initial_spawn_transform = initial_spawn_transform _water_triggers = water_triggers _safe_points = safe_points + _recovery_position_resolver = recovery_position_resolver for water_trigger: PlayerWaterTrigger in _water_triggers: if ( water_trigger != null @@ -183,17 +188,7 @@ func _on_fade_transition_completed( func _respawn_player() -> void: - var target_position: Vector3 = _initial_spawn_transform.origin - var nearest_distance: float = INF - for point: SafeRespawnPoint in _safe_points: - if point == null or not is_instance_valid(point) or not point.enabled: - continue - var distance: float = point.get_horizontal_distance_squared( - _entry_position - ) - if distance < nearest_distance: - nearest_distance = distance - target_position = point.global_position + var target_position := _resolve_recovery_position() target_position.y += respawn_height_offset var respawn_transform: Transform3D = _player.global_transform respawn_transform.basis = _recovery_root_basis @@ -204,6 +199,23 @@ func _respawn_player() -> void: local_respawn_completed.emit(_entry_position) +func _resolve_recovery_position() -> Vector3: + if _recovery_position_resolver.is_valid(): + var resolved: Variant = _recovery_position_resolver.call(_entry_position) + if resolved is Vector3 and (resolved as Vector3).is_finite(): + return resolved as Vector3 + var target_position := _initial_spawn_transform.origin + var nearest_distance := INF + for point: SafeRespawnPoint in _safe_points: + if point == null or not is_instance_valid(point) or not point.enabled: + continue + var distance := point.get_horizontal_distance_squared(_entry_position) + if distance < nearest_distance: + nearest_distance = distance + target_position = point.global_position + return target_position + + func _finish_recovery() -> void: _player.restore_gameplay_orientation_after_recovery() _player.set_water_recovery_active(false) diff --git a/world/world_time_visual_controller.gd b/world/world_time_visual_controller.gd index ec67e85..92614c5 100644 --- a/world/world_time_visual_controller.gd +++ b/world/world_time_visual_controller.gd @@ -206,7 +206,7 @@ func _prepare_runtime_environment() -> bool: runtime_sky.sky_material.duplicate(true) as ShaderMaterial ) if _sky_material == null: - push_error("World time visuals require the NETfishing sky material.") + push_error("World time visuals require the straywild sky material.") return false runtime_sky.sky_material = _sky_material _environment.sky = runtime_sky