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/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..c235601 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. @@ -159,8 +159,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..15e3698 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.19.0-alpha +Alpha 0.19.0 Thank you for trying this early private playtest. -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 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..05b0918 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.19.0-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.19.0.0" +application/product_version="0.19.0.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.19.0-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.19.0-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.19.0-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.19.0" +application/version="0.19.0" 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.19.0-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.19.0-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=190000 +version/name="v0.19.0-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.19.0-alpha/linux-x86_64/straywild.x86_64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 2559af5..8e3e169 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -150,6 +150,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 +223,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 +244,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( @@ -1664,6 +1667,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/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/main/main.gd b/main/main.gd index 1724bd9..e9dc78f 100644 --- a/main/main.gd +++ b/main/main.gd @@ -277,6 +277,7 @@ 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 +295,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 +325,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 +463,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 +472,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 +555,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, @@ -921,6 +920,7 @@ func _initialize_application(dedicated: bool) -> void: _network_fishing, _world_time, _world_weather, + Callable(_test_world, "get_fishable_water_regions"), ) if dedicated: return @@ -972,6 +972,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 +1007,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 +1098,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 +1115,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 +1131,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 +1267,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 +1345,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, @@ -2332,17 +2346,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 @@ -2420,6 +2426,7 @@ func _apply_world( 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, diff --git a/main/main.tscn b/main/main.tscn index b9ad6fb..c1380d6 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"] @@ -59,18 +59,12 @@ [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"] -[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 +133,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 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..822299c 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, 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_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_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..20be022 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 = 11 const GAME_BUILD: String = "prealpha" const MAX_GAME_VERSION_LENGTH: int = 64 const MAX_DISPLAY_NAME_LENGTH: int = 24 @@ -304,7 +304,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: @@ -372,17 +372,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..8b99fd6 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -160,7 +160,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] = {} @@ -432,7 +432,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 @@ -1187,7 +1187,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 diff --git a/network/network_surface_drawing_service.gd b/network/network_surface_drawing_service.gd index 4edc234..c322e5b 100644 --- a/network/network_surface_drawing_service.gd +++ b/network/network_surface_drawing_service.gd @@ -879,7 +879,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 +990,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..a45ad37 100644 --- a/network/network_world_spawn_service.gd +++ b/network/network_world_spawn_service.gd @@ -518,7 +518,12 @@ 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 attempts: int = ( + 48 + if validates_diggable_surface or is_finite(maximum_distance) + else 1 + ) var fallback: Vector3 = Vector3(INF, INF, INF) for _attempt: int in attempts: var roll: float = _rng.randf() * total_area @@ -534,12 +539,22 @@ 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, + ) + ) + if within_requested_distance and valid_diggable_surface: return point + if validates_diggable_surface: + return Vector3(INF, INF, INF) if origin.is_finite() and is_finite(maximum_distance): return origin return fallback 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..64d4ffe 100644 --- a/player/player.gd +++ b/player/player.gd @@ -1036,6 +1036,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. @@ -1635,6 +1637,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 +1670,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 +1690,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 @@ -2352,6 +2367,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 +2416,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 +2452,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 +2668,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 +2682,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, 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/project.godot b/project.godot index 89bf703..d202817 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.19.0-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" [physics] 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..2347d3d 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.19.0-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.19.0-alpha-windows-x86_64.zip" +readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.19.0-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,7 +72,7 @@ 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.sh straywild)" 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..5d15b8e 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" 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..4bfab98 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -416,6 +416,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..ec80d1b 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -971,12 +971,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 +994,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_hotbar_showcase_validation.gd b/tests/fish_hotbar_showcase_validation.gd index c2d687c..753f480 100644 --- a/tests/fish_hotbar_showcase_validation.gd +++ b/tests/fish_hotbar_showcase_validation.gd @@ -197,7 +197,7 @@ 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.PROTOCOL_VERSION == 11) assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17) assert( NetworkProtocol.FISH_QUALITY_CAPABILITY diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd index 3441344..b6e6bfd 100644 --- a/tests/fish_quality_validation.gd +++ b/tests/fish_quality_validation.gd @@ -26,7 +26,7 @@ func _run() -> void: _validate_collection_mastery() _validate_version_four_migration() _validate_fishing_protocol_v2() - assert(NetworkProtocol.PROTOCOL_VERSION == 10) + assert(NetworkProtocol.PROTOCOL_VERSION == 11) assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17) assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11) assert( 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_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/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index 0143322..6a3f6c8 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -490,6 +490,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 +536,11 @@ func _validate_generated_region( -INF, 0.35, ) + _validate_water_recovery_positions( + region, + grass_prop_surface_triangles, + sand_prop_surface_triangles, + ) var anchors := region.get_node( "GatherableAnchors/ReachableTreeTrunks" ) as GatherableAnchorSet3D @@ -868,7 +877,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 +1052,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 +1167,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( @@ -1231,6 +1273,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/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/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..47e795d 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 ) @@ -201,13 +234,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..431ed2e 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() @@ -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" @@ -455,8 +840,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 +867,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..7073bea 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) 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/progression_archive_validation.gd b/tests/progression_archive_validation.gd index 7f98066..c3f9569 100644 --- a/tests/progression_archive_validation.gd +++ b/tests/progression_archive_validation.gd @@ -39,6 +39,15 @@ func _run() -> void: var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path) assert(bool(decoded.get("ok", false))) assert(int((decoded["data"] as Dictionary)["save_version"]) == 10) + 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" @@ -135,6 +144,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 +181,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 +208,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 +229,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/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/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_protocol_validation.gd b/tests/world_spawn_protocol_validation.gd index 29f7fde..d100260 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 == 11) 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() @@ -148,8 +152,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 +181,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/game_ui.gd b/ui/game_ui.gd index 6efea59..3ce0d17 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -81,6 +81,7 @@ 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 const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse" const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu" @@ -259,6 +260,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 +275,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 ): @@ -456,6 +462,7 @@ func setup( discovery, player_jobs, world_time, + world_weather, world_environment, world_sun, ) @@ -1889,7 +1896,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 @@ -2003,7 +2010,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 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..2f9f96f 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 @@ -2779,6 +2791,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 +3746,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/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..d1fc210 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.19.0-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/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/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..780ddb8 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -43,6 +43,12 @@ 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 PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [ Vector2(1.0, 0.0), Vector2(0.70710678, 0.70710678), @@ -90,6 +96,7 @@ 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] = {} func _ready() -> void: @@ -190,7 +197,39 @@ func get_spawn_surface_triangles( return triangles +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() _assign_biomes(records, summary) var spawn_position := Vector3.ZERO @@ -324,6 +363,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() @@ -453,6 +496,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 @@ -1259,6 +1303,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: 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/interactables/fishing_shop_world.tscn b/world/interactables/fishing_shop_world.tscn index 7221005..760f3b6 100644 --- a/world/interactables/fishing_shop_world.tscn +++ b/world/interactables/fishing_shop_world.tscn @@ -1,7 +1,7 @@ [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"] 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.tscn b/world/regions/starter_island_region.tscn index b52b0ea..8f4bc4b 100644 --- a/world/regions/starter_island_region.tscn +++ b/world/regions/starter_island_region.tscn @@ -163,6 +163,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") diff --git a/world/regions/world_region.gd b/world/regions/world_region.gd index a0730e4..792e4ed 100644 --- a/world/regions/world_region.gd +++ b/world/regions/world_region.gd @@ -72,6 +72,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/test_world.gd b/world/test_world.gd index d5ac83c..9f4da5e 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -49,6 +49,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() @@ -207,6 +217,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: 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