Compare commits

...

51 commits

Author SHA1 Message Date
65967816f5 fix multiplayer commands and disconnect recovery 2026-08-13 23:57:15 -04:00
5d7c3d7459 Prepare v0.7.1-alpha release 2026-08-13 22:25:55 -04:00
e54ad1332b Fix rain coverage and in-game session switching 2026-08-13 22:20:43 -04:00
2045d8e288 Fix multiplayer presentation and Fishnet synchronization 2026-08-13 21:58:43 -04:00
31d459b3a7 Prepare v0.6.8-alpha release 2026-08-13 18:59:19 -04:00
dbe0a7b313 Increase Cooler notepad action text sizes 2026-08-13 16:26:05 -04:00
8bc800de36 Refresh character channel masks and add round fox support 2026-08-13 16:25:59 -04:00
552af7a58c Tint the Art Kit marker icon from its selected color 2026-08-13 16:25:49 -04:00
405bdaa2d2 Unify foggy water presentation and remove shoreline ribbons 2026-08-13 16:25:43 -04:00
3576daba16 Refresh fox head and arm channel masks 2026-08-13 15:33:48 -04:00
0076996d39 Update fox pointy-head channel mask 2026-08-13 15:26:02 -04:00
c06d5714c0 Revise Art Kit workflow and refresh fox patterns 2026-08-13 15:24:23 -04:00
117fdbfdaa Polish world, shop, and artwork presentation 2026-08-13 14:53:39 -04:00
7d38bc962b Refine sprint and landing dust effects 2026-08-13 14:23:11 -04:00
c239ca6bd5 Correct foggy ocean horizon blending 2026-08-13 11:13:09 -04:00
5ec62b8465 Refine Bengal pattern and fog blending 2026-08-13 10:46:57 -04:00
2159798d1a Update character model UV maps 2026-08-13 10:21:01 -04:00
51fa6535b6 Add weather presentation and gameplay UI improvements 2026-08-13 01:31:05 -04:00
db6074ddfa Consolidate project documentation and attribution
Add public-alias credits and a tested in-game credits page, centralize project guidance and asset provenance, import the complete authored fur channel maps, and keep starter-island collision synchronized with imported props.
2026-08-12 22:12:11 -04:00
1940ae13e5 Add customizable fur patterns 2026-08-12 16:29:58 -04:00
9729c278dc Merge remote-tracking branch 'origin/main'
# Conflicts:
#	export_presets.cfg
#	scripts/build_playtest.sh
2026-08-12 11:22:50 -04:00
40b8fe0278 License NETfishing code and define asset terms 2026-08-12 10:11:20 -04:00
24b9570463 Honor authored barrier health bands 2026-08-11 22:58:46 -04:00
41192e4ef5 Add identity-backed server moderation 2026-08-11 22:58:33 -04:00
d944367301 Prepare v0.6.7-alpha release 2026-08-11 20:31:02 -04:00
b044d35021 Prepare v0.6.6-alpha release 2026-08-11 20:02:54 -04:00
96df168851 Fix release validation coverage 2026-08-11 17:57:09 -04:00
e791f15e87 Add ambient character blinking 2026-08-11 16:39:20 -04:00
c038eeadea Refine shop, online sessions, and player feedback 2026-08-11 16:35:33 -04:00
2ffcbf51ac Improve public room connection resilience 2026-08-11 10:37:03 -04:00
15551030f5 Prepare v0.6.5-alpha release 2026-08-10 22:07:32 -04:00
d690d74268 Fix multiplayer gameplay and UI interaction edge cases 2026-08-10 21:40:55 -04:00
e589ca9133 feat: add public host NAT traversal 2026-08-10 19:49:06 -04:00
74c82696fe Add dedicated server runtime 2026-08-10 13:56:03 -04:00
e204279b09 Add public room discovery 2026-08-10 10:04:05 -04:00
59ef173ea7 Player model refinements 2026-08-10 07:46:34 -04:00
132fe54150 chore: prepare v0.6.4-alpha release 2026-08-09 22:32:52 -04:00
54e1b8538d feat: refine character animation and gameplay UI 2026-08-09 22:27:33 -04:00
63a5009b33 feat: integrate fishing item transitions 2026-08-09 18:43:54 -04:00
692b6eeb2a Rename barrier power upgrade to rod power 2026-08-09 13:31:31 -04:00
1284619079 Clean repository-owned GDScript warnings 2026-08-09 13:31:30 -04:00
ff536e62a6 Add casting animation phases and stabilize held rod 2026-08-09 13:31:30 -04:00
9e6536adcd Ignore Blender backup files 2026-08-09 12:48:06 -04:00
d73fc76698 Add wind motion to foliage 2026-08-09 12:44:45 -04:00
93c1cd4421 Add daily dusk music cue 2026-08-09 12:44:45 -04:00
deab51a18a Fix casting beneath overhangs 2026-08-09 12:24:11 -04:00
40696b6bd9 Add character calls, settings controls, and gameplay fixes 2026-08-09 11:22:35 -04:00
7b2898c11a Prepare v0.6.3-alpha release 2026-08-08 22:47:38 -04:00
7c8cc9276e Merge remote-tracking branch 'origin/main' 2026-08-08 22:42:04 -04:00
72d3a303f5 Add expanded fishing gear and presentation 2026-08-08 22:39:50 -04:00
9cd09ffe47 Update README.md 2026-08-08 21:19:31 +00:00
318 changed files with 16173 additions and 2402 deletions

3
.gitignore vendored
View file

@ -2,3 +2,6 @@
.godot/
/android/
/builds/
# Blender backup files
*.blend1

135
ASSET-LICENSE.md Normal file
View file

@ -0,0 +1,135 @@
# NETfishing Asset License 1.0
Copyright © 2026 Woofmeow. All rights reserved except for the permissions
expressly granted below.
## 1. Scope
This license applies to original NETfishing artwork, graphics, textures,
models, animations, music, sound recordings, narrative content, visual
designs, characters, and other creative assets owned by the NETfishing owners,
publishing as Woofmeow (the **“NETfishing Assets”**).
It does not apply to:
- software source code identified as GPL-3.0-or-later;
- third-party material identified in the source repository's
`docs/ATTRIBUTION.md` or the packaged `ATTRIBUTION.md` notice;
- material accompanied by a different license; or
- the NETfishing 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:
- 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
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
of the game or represent a fork, clone, sequel, or separate game.
## 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
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
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.
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
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;
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.0. This Mod is unofficial and is not endorsed
> by Woofmeow.
4. identify material created by the Mod author separately from NETfishing
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
Assets in paid products requires separate written permission from both
NETfishing owners.
## 5. Uses not permitted
Except with separate written permission from both NETfishing owners, you may
not use or distribute NETfishing Assets:
- in a fork, clone, remake, sequel, or standalone game;
- with a modified distribution of the GPL-covered NETfishing code that is not
Official NETfishing Software;
- 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.
The GPL permits forks and redistribution of the GPL-covered code. This asset
license does not permit those forks to carry NETfishing Assets. A code fork
must remove or replace the NETfishing Assets and reserved branding unless both
owners grant separate written permission.
## 6. Ownership of Mods
You retain ownership of your original Mod material. The NETfishing owners
retain ownership of the NETfishing Assets and adaptations of those assets to
the extent provided by applicable law.
This license does not require you to assign your original Mod material to the
NETfishing owners. It also does not grant you ownership of the underlying
NETfishing Assets.
## 7. 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
within sixty days after it has been cured.
Termination does not limit remedies available for activity outside this
license.
## 8. No warranty
THE NETFISHING 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.
## 9. Additional permission
Uses not granted here require written permission from both NETfishing owners.
A permission granted to one person or project does not amend this license for
others.
This is a custom asset license, not an open-source software license or a
Creative Commons license.

View file

@ -4,6 +4,12 @@ NETfishing is currently developed through focused changes on the repository's
active development branch. Coordinate scope with the project owner before
starting substantial work.
By intentionally submitting material for inclusion, contributors agree to the
applicable grants and representations in
[`CONTRIBUTOR-TERMS.md`](CONTRIBUTOR-TERMS.md). Maintainers must preserve an
affirmative record of that agreement. Contact the owners before submitting if
separate written terms are needed.
## Change discipline
- Keep a change focused; avoid unrelated cleanup.
@ -15,7 +21,10 @@ starting substantial work.
- Keep presentation-only systems out of save files and network messages.
- Store runtime assets under repository-owned paths; never reference a
workstation sync or temporary directory.
- Record the source and license of newly imported assets.
- Record the source and license of newly imported assets in
[`docs/ATTRIBUTION.md`](docs/ATTRIBUTION.md).
- Do not assume the GPL code license applies to project assets; observe
[`ASSET-LICENSE.md`](ASSET-LICENSE.md) and third-party terms.
- Do not commit `.godot/`, test data, logs, captures, or build outputs.
## Before requesting review
@ -26,7 +35,8 @@ scripts/run_validations.sh quick
git status --short
```
Choose additional focused tests from [`docs/TESTING.md`](docs/TESTING.md).
Choose additional focused tests from the validation section of
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md#validation).
Changes involving networking should run the network suite and a real
two-process check. Visual changes still require graphical review; headless
tests are supporting evidence, not a replacement.
@ -46,3 +56,7 @@ Follow the conventions already present in the surrounding GDScript, scene, and
resource files. Prefer typed values, named constants, shared resources, and
small domain-specific services. Avoid duplicating protocol definitions or
using display strings as persistent identifiers.
New standalone source files should use `SPDX-License-Identifier:
GPL-3.0-or-later` where the file format permits comments. Do not add that
identifier to asset files governed by the NETfishing Asset License.

94
CONTRIBUTOR-TERMS.md Normal file
View file

@ -0,0 +1,94 @@
# NETfishing contributor terms
These terms apply to contributions intentionally submitted for inclusion in
NETfishing after these terms are published. They 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
Woofmeow and are publicly credited as Voyager and Endeavour. “Project” means
the official NETfishing game, its servers, tools, documentation, websites,
ports, updates, and promotional materials.
## 1. Your ownership and authority
You retain copyright in your original contribution. By submitting it, you
represent that:
- you created it or otherwise have authority to grant these permissions;
- its submission does not knowingly violate another partys copyright,
patent, trademark, privacy, contract, or other rights;
- you have disclosed relevant third-party material and license terms; and
- if an employer or another organization controls the contribution, you have
obtained its permission before submitting.
Do not submit confidential information, unlawfully obtained material, or
content whose origin and permitted use cannot be documented.
## 2. Code contributions
You license an accepted software-code contribution under the GNU General
Public License, version 3 or, at the recipients option, any later version
(GPL-3.0-or-later).
You also grant each recipient a perpetual, worldwide, nonexclusive,
royalty-free patent license under patent claims you can license that are
necessarily infringed by your contribution alone or by its combination with
the Project as submitted. This patent license does not cover later
combinations that are unrelated to your submitted contribution.
## 3. Asset, writing, and documentation contributions
For an accepted contribution consisting of artwork, models, animation, audio,
music, writing, level content, branding material, or other non-code creative
work, you grant the Owners a perpetual, worldwide, nonexclusive,
royalty-free, transferable, and sublicensable license to:
- use, reproduce, modify, adapt, translate, and create derivative works;
- distribute, publicly display, publicly perform, transmit, and make
available the contribution;
- include it in free and commercial Project releases, ports, updates,
merchandise, marketing, and archival materials; and
- license it to Project users under `ASSET-LICENSE.md` or a later Project asset
license jointly approved by the Owners.
This grant is limited to NETfishing, its promotion, and official successor or
derived NETfishing products. It does not transfer your copyright or authorize
the Owners to sell your contribution as an unrelated standalone asset.
## 4. Documentation accompanying code
Documentation submitted as an ordinary part of a code contribution may be
distributed under GPL-3.0-or-later with that code. Project credits and
provenance records may be reproduced in any medium needed to document the
Project and its releases.
## 5. Attribution
Provide the name or pseudonym you want used for credit. The Owners will make a
reasonable effort to preserve material attribution, subject to formatting,
privacy, platform, and practical limitations.
Attribution does not imply that a contributor endorses every Project change
or release.
## 6. No automatic ownership, employment, or payment
Submitting a contribution does not provide ownership of NETfishing, create an
employment, agency, partnership, or fiduciary relationship, or create a right
to compensation. Any payment, ownership, or employment arrangement requires a
separate written agreement signed by the appropriate parties.
## 7. Acceptance
A contribution is not accepted merely because it was posted, uploaded, or
discussed. The Owners or their authorized maintainers may accept, modify,
decline, or later remove it.
By intentionally submitting a patch, pull request, asset, or other material
for Project inclusion after receiving notice of these terms, you agree to the
applicable grants above. Maintainers should record the contributors
affirmative agreement in the pull request, issue, email, or another durable
written record.
If you cannot agree to these terms, do not submit the contribution; contact
the Owners to discuss a separate written arrangement.

View file

@ -1,52 +0,0 @@
# Credits
## Project direction
NETfishing is designed, directed, reviewed, and released by its project
creator. A public-facing creator or studio credit has not yet been recorded in
this repository.
## Development assistance
Development has used automated coding assistance for implementation,
investigation, testing, documentation, and review. Design choices, supplied
artwork, acceptance decisions, repository operations, and releases remain
human-controlled.
This disclosure is intentionally about process. Commit style, code style, or
repository metadata cannot reliably prove whether a particular change was
written with or without assistance.
## Fonts
- Tuffy: Thatcher Ulrich, Karoly Barta, and Michael Evans. The bundled license
dedicates the work to the public domain; see `ui/fonts/Tuffy-LICENSE.txt`.
- Seattle Avenue: bundled as `ui/fonts/seattle_avenue.otf`. Author and
redistribution terms still need to be recorded before public distribution.
## Music
- `audio/music/title/as_in_four_wolves.ogg`: original title/ambient music by
the project owner.
## Sound effects
- “Spinning reel.wav” by tosha73, used as the source for the fishing fight and
manual-reeling loops. Freesound sound 509902, Creative Commons Zero (CC0):
https://freesound.org/s/509902/
- “Gentle Ocean Waves Loop” by kkenny101, used for the saltwater shoreline
ambience. Freesound sound 852826, Creative Commons Zero (CC0):
https://freesound.org/s/852826/
- Rain Loop Ontario by Ayton -- https://freesound.org/s/212799/ -- License:
Attribution 3.0
- “Quick Water Droplet” by qubodup, used for the bobber water-impact sound.
Freesound sound 792931, Creative Commons Zero (CC0):
https://freesound.org/s/792931/
## Artwork
- 2D artwork: Rheannon Eisworth, contributor.
- 3D models: original work by the project owner.
Asset categories and release-record requirements are listed in
[`docs/ASSET-PROVENANCE.md`](docs/ASSET-PROVENANCE.md).

232
LICENSE Normal file
View file

@ -0,0 +1,232 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for software and other kinds of works.
The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and modification follow.
TERMS AND CONDITIONS
0. Definitions.
“This License” refers to version 3 of the GNU General Public License.
“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
A “covered work” means either the unmodified Program or a work based on the Program.
To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
1. Source Code.
The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
The Corresponding Source for a work in source code form is that same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
7. Additional Terms.
“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
11. Patents.
A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.

View file

@ -8,6 +8,9 @@ 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
co-owners publicly credited as Voyager and Endeavour.
## Requirements
- Godot 4.7.x with the GL Compatibility renderer
@ -35,8 +38,8 @@ Run the fast, deterministic suite before submitting a change:
scripts/run_validations.sh quick
```
Additional suites and individual commands are documented in
[`docs/TESTING.md`](docs/TESTING.md).
Additional suites and individual commands are documented in the
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md#validation) validation section.
## Building
@ -65,18 +68,26 @@ scripts do not create or publish tags.
- `tests/`: focused Godot validation scripts
- `scripts/`: build, bake, and validation entry points
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and
[`world/README-WORLD-AUTHORING.md`](world/README-WORLD-AUTHORING.md) for more
detail.
All maintained project guidance starts at the
[`docs/` documentation index](docs/README.md). Architecture, content policy,
world and UI authoring, and testing are consolidated in
[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
## Authorship and assets
## Licensing
NETfishing uses a mixture of original assets and
software-development assistance. The project records this plainly rather than
claiming that repository history can prove how each line was produced. See
[`CREDITS.md`](CREDITS.md) and
[`docs/ASSET-PROVENANCE.md`](docs/ASSET-PROVENANCE.md).
- 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, but not use in forks, clones, or
unrelated products.
- The NETfishing 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).
- Contributions are accepted under [CONTRIBUTOR-TERMS.md](CONTRIBUTOR-TERMS.md).
No project-wide source license has been selected yet. Do not assume permission
to redistribute the source or bundled assets beyond rights stated in their
individual license records.
Material boundaries and binary-release requirements are recorded in that same
[`docs/ATTRIBUTION.md`](docs/ATTRIBUTION.md) reference.
Copyright © 2026 Woofmeow.

42
TRADEMARKS.md Normal file
View file

@ -0,0 +1,42 @@
# NETfishing 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.
Their presence in a GPL-licensed repository does not grant trademark rights or
imply permission to represent another product as official.
No trademark registration is asserted by this notice. Applicable rights may
exist through use and other law.
## Permitted references
You may use the names NETfishing and Woofmeow truthfully to:
- refer to the official game or development team;
- state that software interoperates with or is a Mod for NETfishing;
- 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
name and presentation remain clearly distinct and it displays an unofficial
notice.
## Forks and redistributed code
The GNU GPL permits forks and redistribution of covered code. A fork or
modified distribution must:
- use a distinct product name;
- remove NETfishing and Woofmeow logos, icons, and official trade dress;
- avoid confusingly similar names, domains, package identifiers, and account
names; and
- clearly identify itself as independent and unofficial.
These branding terms do not limit rights granted by the GPL to the software
code or lawful nominative, descriptive, comparative, or fair use.
Permission for broader branding use must be obtained in writing from both
NETfishing owners.

View file

@ -0,0 +1,7 @@
[plugin]
name="NETfishing Shoreline Auto-Baker"
description="Rebuilds configured static shoreline ribbons after authored terrain reimports."
author="woofmeow"
version="1.0.0"
script="shoreline_auto_baker.gd"

View file

@ -0,0 +1,66 @@
@tool
extends EditorPlugin
const TERRAIN_RESOURCE := (
"res://art/exported/environment/terrain/starter_island.glb"
)
const REGION_SCENE := "res://world/regions/starter_island_region.tscn"
var _bake_pending := false
func _enter_tree() -> void:
var filesystem := get_editor_interface().get_resource_filesystem()
if not filesystem.resources_reimported.is_connected(_on_resources_reimported):
filesystem.resources_reimported.connect(_on_resources_reimported)
func _exit_tree() -> void:
var filesystem := get_editor_interface().get_resource_filesystem()
if filesystem.resources_reimported.is_connected(_on_resources_reimported):
filesystem.resources_reimported.disconnect(_on_resources_reimported)
func _on_resources_reimported(resources: PackedStringArray) -> void:
if not resources.has(TERRAIN_RESOURCE) or _bake_pending:
return
_bake_pending = true
call_deferred("_bake_configured_shorelines")
func _bake_configured_shorelines() -> void:
_bake_pending = false
var packed_region := ResourceLoader.load(
REGION_SCENE,
"PackedScene",
ResourceLoader.CACHE_MODE_REPLACE,
) as PackedScene
if packed_region == null:
push_error("Shoreline auto-bake could not load %s." % REGION_SCENE)
return
var region := packed_region.instantiate() as Node3D
add_child(region)
var baker := region.get_node_or_null(
"ShorelineRibbonBaker"
) as ShorelineRibbonBaker
if baker == null:
push_error("Shoreline auto-bake found no configured baker.")
region.queue_free()
return
var results := baker.rebuild_all()
for result: Dictionary in results:
if result.get("skipped", false):
continue
var output_path: String = result.get("output_path", "")
if not output_path.is_empty():
ResourceLoader.load(
output_path,
"ArrayMesh",
ResourceLoader.CACHE_MODE_REPLACE,
)
print(
"Shoreline auto-bake: %s, %d loops, %d triangles"
% [output_path, result.loop_count, result.triangle_count]
)
region.queue_free()
get_editor_interface().get_resource_filesystem().scan()

View file

@ -0,0 +1 @@
uid://cbk4xpalei5v3

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -1,24 +0,0 @@
# Drop-in facial features
Facial feature textures are discovered when the game starts. Put new PNGs in
the matching directory, restart the game, and the option will appear in the
Profile customization page.
```text
eyes/<id>.png
noses/<id>.png
mouth/<id>.png
```
The category directory is authoritative. A category prefix is optional, so
both `sleepy.png` and `eyes_sleepy.png` become the option ID `sleepy`.
Filenames are normalized to lowercase snake_case. Use stable names because
the resulting IDs are stored in appearance snapshots and sent to peers.
Keep each PNG on the established facial-feature canvas with an RGBA
transparent background. Godot imports the PNG automatically in the editor;
the runtime registry ignores `.import` files and other non-PNG files.
The current game bundles its facial assets under `res://`. Adding a file to a
development checkout is picked up on the next startup. An exported build must
be rebuilt to include newly added `res://` files in its package.

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://cxpiqjwgff0xr"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
uid="uid://b2ufsni8mnpmq"
path="res://.godot/imported/eyes_closed.png-80789498475c2cfbcb6e3ac14e067a76.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
source_file="res://art/exported/characters/faces/eyes/eyes_closed.png"
dest_files=["res://.godot/imported/eyes_closed.png-80789498475c2cfbcb6e3ac14e067a76.ctex"]
[params]
@ -38,6 +38,3 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bm0kkevv62mu0"
path="res://.godot/imported/art_kit.svg-d4bd09e1da2e2339f7d505ee23d771f8.ctex"
uid="uid://44cqu18imydc"
path="res://.godot/imported/mouth_open_ah.png-2710d397264f57dbe8f9bac82dd4b0de.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/placeholder/art_kit.svg"
dest_files=["res://.godot/imported/art_kit.svg-d4bd09e1da2e2339f7d505ee23d771f8.ctex"]
source_file="res://art/exported/characters/faces/mouth/mouth_open_ah.png"
dest_files=["res://.godot/imported/mouth_open_ah.png-2710d397264f57dbe8f9bac82dd4b0de.ctex"]
[params]
@ -38,6 +38,3 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://trqvs4dgd4rt"
path="res://.godot/imported/fish_coin.svg-9d00b5918d94a555938a84ee65e06386.ctex"
uid="uid://c78hwv0t3ph47"
path="res://.godot/imported/mouth_open_oh.png-10fc59160b782879c415074831421088.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/placeholder/fish_coin.svg"
dest_files=["res://.godot/imported/fish_coin.svg-9d00b5918d94a555938a84ee65e06386.ctex"]
source_file="res://art/exported/characters/faces/mouth/mouth_open_oh.png"
dest_files=["res://.godot/imported/mouth_open_oh.png-10fc59160b782879c415074831421088.ctex"]
[params]
@ -38,6 +38,3 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b12jrc4ao3r16"
path="res://.godot/imported/mouth_open_smile.png-2561ffea934645e2eac40ab3a0a6395a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/faces/mouth/mouth_open_smile.png"
dest_files=["res://.godot/imported/mouth_open_smile.png-2561ffea934645e2eac40ab3a0a6395a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b2e0uxfn187gu"
path="res://.godot/imported/body_arms_bengal.png-350ed15f218f0efa6ae7573ad27d3c65.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/bengal/body_arms_bengal.png"
dest_files=["res://.godot/imported/body_arms_bengal.png-350ed15f218f0efa6ae7573ad27d3c65.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cn71sswmj0tue"
path="res://.godot/imported/body_main_bengal.png-59815350b807220c52317e683582847e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/bengal/body_main_bengal.png"
dest_files=["res://.godot/imported/body_main_bengal.png-59815350b807220c52317e683582847e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://rv36ws5wuakb"
path="res://.godot/imported/head_round_bengal.png-dfa502a4514c1605f5c8aff839dd8765.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/bengal/head_round_bengal.png"
dest_files=["res://.godot/imported/head_round_bengal.png-dfa502a4514c1605f5c8aff839dd8765.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://p5cg2bwrxf7v"
path="res://.godot/imported/body_arms_fox.png-91ab21ce544fdc29b8a0ebf3e54ef7cd.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/fox/body_arms_fox.png"
dest_files=["res://.godot/imported/body_arms_fox.png-91ab21ce544fdc29b8a0ebf3e54ef7cd.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://wcsadkjjpaei"
path="res://.godot/imported/body_main_fox.png-25e4fa03a48053f6b37b3bbadb1f01ba.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/fox/body_main_fox.png"
dest_files=["res://.godot/imported/body_main_fox.png-25e4fa03a48053f6b37b3bbadb1f01ba.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c4vlbrp7qevvo"
path="res://.godot/imported/head_pointy_fox.png-ef3535a6c05a49aae36fe5514844a3df.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/fox/head_pointy_fox.png"
dest_files=["res://.godot/imported/head_pointy_fox.png-ef3535a6c05a49aae36fe5514844a3df.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bwiwt76kyyevw"
path="res://.godot/imported/head_round_fox.png-27dc2125a4e5b85a622291762225a303.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/fox/head_round_fox.png"
dest_files=["res://.godot/imported/head_round_fox.png-27dc2125a4e5b85a622291762225a303.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cyr0wcavb1cmi"
path="res://.godot/imported/tails_fox_fox.png-061a224c36e90f10b33d37e616ff7e94.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://art/exported/characters/patterns/fox/tails_fox_fox.png"
dest_files=["res://.godot/imported/tails_fox_fox.png-061a224c36e90f10b33d37e616ff7e94.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

@ -0,0 +1,45 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://3uq4cxiomttt"
path="res://.godot/imported/netfishing_character_source.glb-9c98ee44b142c5b3d06f0838b7d2c9c4.scn"
[deps]
source_file="res://art/source/characters/base/netfishing_character_source.glb"
dest_files=["res://.godot/imported/netfishing_character_source.glb-9c98ee44b142c5b3d06f0838b7d2c9c4.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

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

View file

@ -28,7 +28,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""

BIN
audio/music/world/craft.mp3 Normal file

Binary file not shown.

View file

@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://c4y8puv0n6xk8"
path="res://.godot/imported/craft.mp3-ba8d0c0c16ba90585f5d8eb4f37eb87d.mp3str"
[deps]
source_file="res://audio/music/world/craft.mp3"
dest_files=["res://.godot/imported/craft.mp3-ba8d0c0c16ba90585f5d8eb4f37eb87d.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View file

@ -19,3 +19,9 @@ bus/2/mute = false
bus/2/bypass_fx = false
bus/2/volume_db = 0.0
bus/2/send = &"Master"
bus/3/name = &"Environment"
bus/3/solo = false
bus/3/mute = false
bus/3/bypass_fx = false
bus/3/volume_db = 0.0
bus/3/send = &"Master"

View file

@ -1,72 +0,0 @@
# Architecture
## Composition
`main/main.tscn` is the application composition root. It owns long-lived
services, the active world, player container, save/settings managers, and the
pixelated UI viewport. Services are scene-owned rather than global autoloads,
which makes their dependencies visible in the main scene and allows focused
tests to instantiate the same composition.
The main script coordinates title, session, world, and UI lifecycle. Domain
logic remains in typed resources and services rather than being stored solely
in controls.
## Domain boundaries
- `fish/` defines species, availability, catches, pools, quality, and selection.
- `fishing/` owns cast/chase state and resolves authoritative fishing surfaces.
- `inventory/`, `items/`, `economy/`, `progression/`, and `jobs/` own persistent
player-facing game state.
- `world/` owns authored map composition and presentation. Water type is a
shared typed definition used by water bodies, fish habitat, fishing
validation, Logbook classification, and saltwater shoreline presentation.
- `ui/` observes and invokes domain services. It does not define stable item or
fish identity.
Resource files (`.tres`) are authored data. Stable IDs, not display names or
node names, connect persistent and networked state to that data.
## Networking
`NetworkSession` owns ENet session lifecycle and peer registration. Dedicated
network services validate and replicate bounded domains such as fishing,
sales, shops, item use, profiles, jobs, mail, chat, drawings, time, and weather.
The host is authoritative. Clients submit requests or evidence; the host
derives trusted context from registered peers, authoritative regions, and
server-owned state before mutating inventory, wallet, progression, or shared
world state. See [`decisions/0001-host-authority.md`](decisions/0001-host-authority.md).
Protocol compatibility is defined in `network/network_protocol.gd`. A visible
release version is not a reason to change the protocol number.
## Persistence
`PlayerDataRoot` selects and validates a portable data root. Stores receive
paths from that owner rather than inventing unrelated locations. Progression is
written by `PlayerSaveManager`; device settings and social/identity stores have
separate formats and lifecycles.
Save migrations are sequential and explicit. Existing catches and ownership
are keyed by stable IDs so authored metadata can evolve without rewriting
historical records. Current source constants—not documentation—are
authoritative for format versions.
## Presentation
Gameplay is rendered in 3D with the GL Compatibility renderer. The main UI is
presented through a uniformly scaled SubViewport. Shared UI components and
palette resources prevent page-specific geometry and style drift.
World presentation systems (time/weather visuals, procedural sky and water,
shoreline ribbons, player blob shadows) do not own gameplay collision or
network authority. Generated shoreline meshes are deterministic presentation
resources baked from explicitly configured static terrain.
## Validation
Tests are executable Godot `SceneTree` scripts. Some are pure content/domain
checks, some instantiate the main scene, and multiplayer checks run paired host
and client processes on loopback. The repository runner provides isolation and
consistent entry points; see [`TESTING.md`](TESTING.md).

View file

@ -1,109 +0,0 @@
# Asset provenance
This document records where runtime assets came from and what must be verified
before redistribution. A Git commit proves when bytes entered this repository;
it does not by itself prove authorship or licensing.
## Intake policy
For every new external or supplied asset, record:
- creator or supplying party;
- original source location or delivery channel;
- source filename and repository destination;
- source and destination SHA-256 when copied without modification;
- license or explicit permission;
- any permitted transformations;
- the importing commit or release.
Runtime resources must use `res://` paths. Workstation Sync folders are intake
locations only and must never appear in scenes or resources.
## Current inventory
| Asset family | Repository location | Recorded provenance |
| --- | --- | --- |
| Fish art and portraits | `fish/species/`, `art/exported/fish/` | 2D artwork by contributor Rheannon Eisworth. |
| Item icons | `items/icons/` | 2D artwork by contributor Rheannon Eisworth. |
| Inventory notepad | `art/ui/ui_notepad.png` | 2D artwork by contributor Rheannon Eisworth; integrated without a runtime external path. |
| Environment textures | `art/exported/environment/textures/` | 2D artwork by contributor Rheannon Eisworth. |
| UI patterns | `art/patterns/` | 2D artwork by contributor Rheannon Eisworth. |
| Character and world models | `art/exported/` | Original 3D models by the project owner. Any embedded 2D artwork is by Rheannon Eisworth. |
| Tuffy font | `ui/fonts/Tuffy_Bold.otf` | Public-domain dedication in `ui/fonts/Tuffy-LICENSE.txt`. |
| Seattle Avenue font | `ui/fonts/seattle_avenue.otf` | License and attribution not present; resolve before public distribution. |
| Title music | `audio/music/title/as_in_four_wolves.ogg` | Original music composed and owned by the project owner. |
| Fishing fight loop | `audio/sfx/fishing/fighting.wav` | Edited from “Spinning reel.wav” by Freesound user tosha73, sound 509902, CC0. |
| Manual-reeling loop | `audio/sfx/fishing/reeling.wav` | Edited from the same CC0 “Spinning reel.wav” source. |
| Saltwater wave ambience | `audio/ambience/waves.wav` | “Gentle Ocean Waves Loop” by Freesound user kkenny101, sound 852826, CC0. |
| Bobber water impact | `audio/sfx/fishing/bobber.wav` | “Quick Water Droplet” by Freesound user qubodup, sound 792931, CC0. |
### Fishing fight loop source record
- Source page: https://freesound.org/s/509902/
- Creator: tosha73
- Source title: `Spinning reel.wav`
- License: Creative Commons Zero (CC0)
- Downloaded source filename: `509902__tosha73__spinning-reel.wav`
- Downloaded source SHA-256:
`9741afdb1ec9c73f5e039f0e5ac174998cb0a1f737b626531e63919d4d232fad`
- Audacity working project: `fighting.aup3`
- Audacity project SHA-256:
`9928f077ecf48d01416f8d05700707e1b07d0f731846db80e27116f5b82dbbf5`
- Runtime edit source/destination SHA-256:
`b6f3f2f94bfaf9254ac04dd96b1fd28c15e1011a1db44bb3e70eeaa95de3c212`
- Runtime format: 48 kHz, 16-bit, stereo PCM WAV; 2.593479 seconds;
configured as a forward loop by the fishing presentation.
- Manual-reeling Audacity project: `reeling.aup3`
- Manual-reeling Audacity project SHA-256:
`20ad52f6bdd16ae6561f06f8355fe309e95023e815053fd5dfbd5a5ce3c1f9f9`
- Manual-reeling runtime source/destination SHA-256:
`e403d5b7c8af1da4b28599cc0f51de3eda70d9e9296db27852fe7dc95617a29d`
- Manual-reeling runtime format: 48 kHz, 16-bit, stereo PCM WAV;
2.388083 seconds; configured as a forward loop.
- The downloaded source and Audacity projects remain in the project owner's
source-work archive; only the finished runtime edits ship in the game.
### Saltwater wave ambience source record
- Source page: https://freesound.org/s/852826/
- Creator: kkenny101
- Source title: `Gentle Ocean Waves Loop`
- License: Creative Commons Zero (CC0)
- Downloaded source filename:
`852826__kkenny101__gentle-ocean-waves-loop.wav`
- Downloaded source and runtime source/destination SHA-256:
`f93e9890583f072ac52cf197b8b5942397d3d5e889fbdb9112a0776142f50229`
- Runtime format: 48 kHz, 24-bit, mono PCM WAV; 21.769917 seconds;
configured as a forward loop.
- The downloaded source remains in the project owner's source-work archive;
only the finished runtime loop ships in the game.
### Bobber water-impact source record
- Source page: https://freesound.org/s/792931/
- Creator: qubodup
- Source title: `Quick Water Droplet`
- License: Creative Commons Zero (CC0)
- Downloaded source filename: `792931__qubodup__quick-water-droplet.wav`
- Downloaded source and runtime source/destination SHA-256:
`aef0d16384efed4a7b071c4d80c21597617f882f4e0c1d5295ac194d315eafb7`
- Runtime format: 48 kHz, 16-bit, mono PCM WAV; 0.197146 seconds; one-shot.
- The downloaded source remains in the project owner's source-work archive;
only the finished runtime sound ships in the game.
## Generated resources
Godot `.import` sidecars and deterministic shoreline `.tres` meshes are derived
repository resources, not original artwork. `.godot/imported/`, editor caches,
captures, exports, and temporary test data are not tracked.
## Release gate
Before a public binary or source release:
1. Resolve the Seattle Avenue font license and attribution gap above.
2. Record the public owner/studio credit and the contributor's redistribution
permission in the chosen project license or release records.
3. Confirm the project-wide source/content license selected by the owner.
4. Search exported resources for external filesystem paths.
5. Preserve required third-party notices with the package.

327
docs/ATTRIBUTION.md Normal file
View file

@ -0,0 +1,327 @@
# Attribution, licensing, and asset provenance
This is the authoritative project record for ownership, credits, license
boundaries, third-party notices, asset provenance, and redistribution checks.
A Git commit proves when bytes entered the repository; it does not by itself
prove authorship or licensing.
## Ownership and credits
NETfishing is jointly owned, developed, and published by its creators,
publicly credited as Voyager and Endeavour and operating collectively as the
independent team Woofmeow.
- Voyager: co-owner, developer, project director, creator of the game's 3D
art, and composer of the original title/ambient track
`audio/music/title/as_in_four_wolves.ogg`, world track
`audio/music/world/craft.mp3`, and animalese placeholder tones.
- Endeavour: co-owner, developer, and creator of the game's 2D art, including
the original fish, environment, UI, and other artwork.
- chillnfill: contributor of original 3D models for the character bodies and
arms, ears and tails, and multiple world props and decorative assets,
including trees and bridges. The contributor requested to be credited as
`chillnfill`.
- Tekgator: creator of the cooler-capacity, reel-speed, and barrier-power shop
icons.
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.
## License map
Do not describe the complete repository as covered only by the GPL.
| Material | Governing terms |
| --- | --- |
| Project-owned software code, shaders, code-oriented scene/resource configuration, tests, build tooling, and server software | GPL-3.0-or-later; source-root `LICENSE` and packaged `GPL-3.0-or-later.txt` |
| Project-owned artwork, textures, models, animation, music, audio, narrative content, characters, and other creative assets | Source-root or packaged `ASSET-LICENSE.md` |
| NETfishing 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 |
| New outside contributions | Source-root `CONTRIBUTOR-TERMS.md` plus any separately signed agreement |
Generated Godot import metadata and deterministic generated resources follow
the terms of their source material or generating code. Repository location
alone does not relicense third-party material.
The GPL permits study, modification, redistribution, and forks of covered
code. It does not grant rights to NETfishing creative assets or branding. The
NETfishing Asset License separately permits qualifying noncommercial add-on
Mods for official NETfishing software; it does not permit those assets to
accompany code forks, clones, standalone games, unrelated products, or
general-purpose asset packs. A GPL fork must replace NETfishing 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
ownership agreement.
## Godot Engine notice
NETfishing uses Godot Engine under the MIT License:
> Copyright (c) 2014-present Godot Engine contributors.
> Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the “Software”), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in
> all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
Godot licensing and bundled third-party-component notices are available at
https://godotengine.org/license/ and
https://github.com/godotengine/godot/blob/master/COPYRIGHT.txt. Godot uses
FreeType for font rendering; portions are copyright The FreeType Project
(https://www.freetype.org) under its applicable license.
## Font notices
- **Tuffy Bold** — Thatcher Ulrich, Karoly Barta, and Michael Evans; dedicated
to the public domain. Runtime file: `ui/fonts/Tuffy_Bold.otf`; original notice:
`ui/fonts/Tuffy-LICENSE.txt`.
- **Seattle Avenue** — JLH Fonts; dedicated to the public domain by the creator.
Runtime file: `ui/fonts/seattle_avenue.otf`; source, hash, and original notice:
`ui/fonts/Seattle-Avenue-LICENSE.txt`.
Public-domain fonts are not governed by the NETfishing Asset License.
## Intake policy
For every new external or supplied asset, record:
- creator or supplying party;
- original source location or delivery channel;
- source filename and repository destination;
- source and destination SHA-256 when copied without modification;
- license or explicit permission;
- any permitted transformations;
- the importing commit or release.
Maintainers must also obtain affirmative agreement to the source-root
`CONTRIBUTOR-TERMS.md` before accepting an outside contribution and retain
that agreement in a durable pull request, issue, email, or signed document.
Runtime resources must use `res://` paths. Workstation Sync folders are intake
locations only and must never appear in scenes or resources.
## Asset inventory and provenance
| Asset family | Repository location | Recorded provenance |
| --- | --- | --- |
| Fish art and portraits | `fish/species/`, `art/exported/fish/` | Original 2D artwork by co-owner Endeavour. |
| Item icons | `items/icons/` | Original 2D artwork by co-owner Endeavour except where separately credited below. |
| Cooler-capacity shop icon | `items/icons/equipment/64_cooler_plus.png` | Original artwork by Tekgator. Repository SHA-256: `b63705be5b78915753c068170323781a90e22b555b57a5b2ca9466e2726539f7`. |
| Reel-speed shop icon | `items/icons/shop/64_speed_plus.png` | Original artwork by Tekgator. Repository SHA-256: `938452798f2be505e06ce3fbef178f52459331fd3bced8763fbc2a80157125ca`. |
| Barrier-power shop icon | `items/icons/shop/64_power_plus.png` | Original artwork by Tekgator. Repository SHA-256: `164d34ce152aa6d75de79b69f0be2d9074f7891057de9636678889028ff98ae7`. |
| Inventory notepad | `art/ui/ui_notepad.png` | Original 2D artwork by co-owner Endeavour; integrated without a runtime external path. |
| Environment textures | `art/exported/environment/textures/` | Original 2D artwork by co-owner Endeavour. |
| UI patterns | `art/patterns/` | Original 2D artwork by co-owner Endeavour. |
| Fur customization channel maps | `art/exported/characters/patterns/` | Original 2D channel-map artwork by co-owner Endeavour. Exact file hashes are recorded below. |
| Character base meshes and appendages | `art/source/characters/base/`, `art/exported/characters/base/`, and derived character resources | Original body and arm models and original ear and tail models by chillnfill. Any embedded original 2D artwork is by Endeavour. |
| World and decorative prop models | `art/source/environment/`, `art/exported/environment/`, and derived world resources | Original prop and decorative models by chillnfill, including trees and bridges. Any embedded original 2D artwork is by Endeavour. |
| 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; source record and hash in `ui/fonts/Seattle-Avenue-LICENSE.txt`. |
| Title music | `audio/music/title/as_in_four_wolves.ogg` | Original music composed and owned by co-owner Voyager. |
| Dusk music | `audio/music/world/craft.mp3` | Original music composed and owned by co-owner Voyager. |
| Fishing fight loop | `audio/sfx/fishing/fighting.wav` | Edited from “Spinning reel.wav” by Freesound user tosha73, sound 509902, CC0. |
| Manual-reeling loop | `audio/sfx/fishing/reeling.wav` | Edited from the same CC0 “Spinning reel.wav” source. |
| Saltwater wave ambience | `audio/ambience/waves.wav` | “Gentle Ocean Waves Loop” by Freesound user kkenny101, sound 852826, CC0. |
| Bobber water impact | `audio/sfx/fishing/bobber.wav` | “Quick Water Droplet” by Freesound user qubodup, sound 792931, CC0. |
| Rain ambience | `audio/ambience/rain_loop_ontario.ogg` | Converted from “Rain Loop Ontario” by Freesound user Ayton, sound 212799, CC BY 3.0. |
| Character bark call | `sound/dialogue/calls/bark.wav` | Edited from `Dog_Bark.wav` by Freesound user ivolipa, sound 328729, CC0. |
| Character meow call | `sound/dialogue/calls/meow.wav` | Edited from `Cat meow.m4a` by Freesound user Christyboy100, sound 495694, Attribution 3.0. |
| Animalese placeholder tones | `sound/dialogue/animalese/placeholder/` | Original tones composed and owned by co-owner Voyager without third-party samples. |
Creative Commons Zero information:
https://creativecommons.org/publicdomain/zero/1.0/. Creative Commons
Attribution 3.0 legal text:
https://creativecommons.org/licenses/by/3.0/legalcode.
### chillnfill 3D model contribution record
- Requested credit: `chillnfill`.
- Original contribution families: character bodies and arms; character ears
and tails; and multiple world props and decorative assets, including trees
and bridges.
- Repository mapping: character contributions are incorporated into the
character source/export families identified above. Prop contributions may
be incorporated into combined environment source and export files rather
than retained as one file per originally supplied model.
- Permission record: because these historical contributions predate the
current `CONTRIBUTOR-TERMS.md`, retain the original permission record or a
later written confirmation covering Project use, modification,
distribution, commercial release, and applicable sublicensing.
### Fur customization channel-map record
Original channel-map artwork by Endeavour. Repository paths preserve the
authored `mesh_style.png` filenames.
| Style and mesh | Source filename | Repository file | SHA-256 |
| --- | --- | --- | --- |
| Bengal spots, body and arms | `body_arms_bengal.png` | `art/exported/characters/patterns/bengal/body_arms_bengal.png` | `fa90bd4e74b0b770ce97bf69d069676f5d4bd1f5d649f89e09638a66b61a8470` |
| Bengal spots, main body | `body_main_bengal.png` | `art/exported/characters/patterns/bengal/body_main_bengal.png` | `8662fa198220cfdbe0aa83bdeeef7b855e20124d74f8481467f750d32b67ee8b` |
| Bengal spots, round head | `head_round_bengal.png` | `art/exported/characters/patterns/bengal/head_round_bengal.png` | `f397ed8d152e3fda8181757a2bbd01925b7b8c2c4362f515d91c7f0fbf3ddfb6` |
| Fox, body and arms | `body_arms_fox.png` | `art/exported/characters/patterns/fox/body_arms_fox.png` | `9930808718f31d84b415902f1ba55ac2a508bb2ac127300e793d73a8a013022d` |
| Fox, main body | `body_main_fox.png` | `art/exported/characters/patterns/fox/body_main_fox.png` | `c65e4390173b8c31c9396edd053f940f0f809e47e01d18042cd7babc712dde9d` |
| Fox, pointy head | `head_pointy_fox.png` | `art/exported/characters/patterns/fox/head_pointy_fox.png` | `71a9d2383e62884dbd3981460fcd985b18805fa907f61ffb16dce08ff9418ddf` |
| Fox, round head | `head_round_fox.png` | `art/exported/characters/patterns/fox/head_round_fox.png` | `86dd7af8a2346faf67359ad668a117a74e2408676137d6b0626403cfcf2aba73` |
| Fox tail | `tails_fox_fox.png` | `art/exported/characters/patterns/fox/tails_fox_fox.png` | `72da9673f038e2c3bb8aca19ed99932085fa3b162cab95366901c4e97153dd3e` |
### Tekgator shop icon contribution record
- Requested credit: `Tekgator`.
- Original contributions: cooler-capacity, reel-speed, and barrier-power shop
icons.
- Repository files and SHA-256:
- `items/icons/equipment/64_cooler_plus.png`:
`b63705be5b78915753c068170323781a90e22b555b57a5b2ca9466e2726539f7`
- `items/icons/shop/64_speed_plus.png`:
`938452798f2be505e06ce3fbef178f52459331fd3bced8763fbc2a80157125ca`
- `items/icons/shop/64_power_plus.png`:
`164d34ce152aa6d75de79b69f0be2d9074f7891057de9636678889028ff98ae7`
- Permission record: retain the original permission record or a later written
confirmation covering Project use, modification, distribution, commercial
release, and applicable sublicensing.
### Fishing fight loop source record
- Source page: https://freesound.org/s/509902/
- Creator: tosha73
- Source title: `Spinning reel.wav`
- License: Creative Commons Zero (CC0)
- Downloaded source filename: `509902__tosha73__spinning-reel.wav`
- Downloaded source SHA-256:
`9741afdb1ec9c73f5e039f0e5ac174998cb0a1f737b626531e63919d4d232fad`
- Audacity working project: `fighting.aup3`
- Audacity project SHA-256:
`9928f077ecf48d01416f8d05700707e1b07d0f731846db80e27116f5b82dbbf5`
- Runtime edit source/destination SHA-256:
`b6f3f2f94bfaf9254ac04dd96b1fd28c15e1011a1db44bb3e70eeaa95de3c212`
- Runtime format: 48 kHz, 16-bit, stereo PCM WAV; 2.593479 seconds;
configured as a forward loop by the fishing presentation.
- Manual-reeling Audacity project: `reeling.aup3`
- Manual-reeling Audacity project SHA-256:
`20ad52f6bdd16ae6561f06f8355fe309e95023e815053fd5dfbd5a5ce3c1f9f9`
- Manual-reeling runtime source/destination SHA-256:
`e403d5b7c8af1da4b28599cc0f51de3eda70d9e9296db27852fe7dc95617a29d`
- Manual-reeling runtime format: 48 kHz, 16-bit, stereo PCM WAV;
2.388083 seconds; configured as a forward loop.
- The downloaded source and Audacity projects remain in the project owner's
source-work archive; only the finished runtime edits ship in the game.
### Saltwater wave ambience source record
- Source page: https://freesound.org/s/852826/
- Creator: kkenny101
- Source title: `Gentle Ocean Waves Loop`
- License: Creative Commons Zero (CC0)
- Downloaded source filename:
`852826__kkenny101__gentle-ocean-waves-loop.wav`
- Downloaded source and runtime source/destination SHA-256:
`f93e9890583f072ac52cf197b8b5942397d3d5e889fbdb9112a0776142f50229`
- Runtime format: 48 kHz, 24-bit, mono PCM WAV; 21.769917 seconds;
configured as a forward loop.
- The downloaded source remains in the project owner's source-work archive;
only the finished runtime loop ships in the game.
### Bobber water-impact source record
- Source page: https://freesound.org/s/792931/
- Creator: qubodup
- Source title: `Quick Water Droplet`
- License: Creative Commons Zero (CC0)
- Downloaded source filename: `792931__qubodup__quick-water-droplet.wav`
- Downloaded source and runtime source/destination SHA-256:
`aef0d16384efed4a7b071c4d80c21597617f882f4e0c1d5295ac194d315eafb7`
- Runtime format: 48 kHz, 16-bit, mono PCM WAV; 0.197146 seconds; one-shot.
- The downloaded source remains in the project owner's source-work archive;
only the finished runtime sound ships in the game.
### Character-call source records
#### Bark
- Source page: https://freesound.org/s/328729/
- Creator: ivolipa
- Source title: `Dog_Bark.wav`
- License: Creative Commons Zero (CC0)
- Downloaded source filename: `328729__ivolipa__dog_bark.wav`
- Downloaded source SHA-256:
`c3d542dee7bfcc1901428e0c1408c0bd522d778d8e5e1dbca156cab008f3d712`
- Runtime edit source/destination SHA-256:
`5ac2f32f33724a2fc232d725c5c37d4cfe3e93b3c1f19aa4177d2c8e3009ec78`
- Runtime format: 44.1 kHz, 16-bit, stereo PCM WAV; 0.446780 seconds;
one-shot.
#### Meow
- Source page: https://freesound.org/s/495694/
- Creator: Christyboy100
- Source title: `Cat meow.m4a`
- License: Attribution 3.0
- Downloaded source filename: `495694__christyboy100__cat-meow.m4a`
- Downloaded source SHA-256:
`afa3f6ef7afa2f7fee1e634d5505d2101cbba4421f8d8ff9adf562939c7a18f8`
- Converted working WAV SHA-256:
`a505e6e230e7d9d76b65c46270d97a35a1acc510b43aa5e7343d7b04f424cade`
- Runtime edit source/destination SHA-256:
`cce22ce1fbbc9325126a153aedc4526fde1aacf78780574fb432197dd44eb6b3`
- Runtime format: 44.1 kHz, 16-bit, stereo PCM WAV; 0.605420 seconds;
one-shot.
- The downloaded sources and working edits remain in the project owner's
source-work archive; only the finished runtime sounds ship in the game.
### Rain ambience source record
- Source page: https://freesound.org/s/212799/
- Creator: Ayton
- Source title: `Rain Loop Ontario`
- License: Creative Commons Attribution 3.0
- Downloaded source filename: `212799__ayton__rain-loop-ontario.aiff`
- Downloaded source SHA-256:
`b491fc036b78a274d9ac7f6acbd014af1b5d24f5cba9bd7e3fdc05c5432ee66f`
- Runtime edit SHA-256:
`b1979609b20fe34ed6f8902f3de65ae787a09abd71429e8aab850e87b74a4fa2`
- Changes: converted from AIFF to Ogg Vorbis and configured as looping
weather ambience.
## Generated resources
Godot `.import` sidecars and deterministic shoreline `.tres` meshes are derived
repository resources, not original artwork. `.godot/imported/`, editor caches,
captures, exports, and temporary test data are not tracked.
## Release gate
Before a public binary or source release:
1. Search exported resources for external filesystem paths.
2. Include or make readily accessible the complete GPL text and identify the
exact corresponding source tag or commit.
3. Include `ASSET-LICENSE.md`, `TRADEMARKS.md`, this attribution/provenance
record, both font notices, and every applicable third-party license.
4. Retain the complete Godot Engine MIT notice above.
5. Preserve third-party terms without adding incompatible restrictions.
6. Avoid referring to a dirty or unavailable source revision.
7. Publish or offer equivalent access to exact corresponding GPL source for
every distributed binary release.
Desktop and PortMaster build scripts stage these authoritative files as
sidecars. Godot export presets include them in exported resource packs for
platforms where sidecar documents are not normally visible. Store and release
pages should link to the exact source revision and public notices.

View file

@ -1,41 +0,0 @@
# Balance and content notes
Balance is authored data and should change deliberately. This document records
the policy; exact current values remain authoritative in `.tres` resources and
typed scripts.
## Fish
- Stable fish IDs and catalog numbers must not be changed for display cleanup.
- Species availability may constrain water type, time, weather, bait, and other
authored context.
- Location pools control selection weight; the global catalog remains
comprehensive.
- Freshwater/saltwater habitat is validated by the host in addition to pool
membership.
- Value, rarity, quality, and weight ranges should be reviewed together because
they affect economy and catch difficulty.
## Economy and progression
- Sales and purchases are host-authoritative and must mutate inventory and
wallet exactly once.
- Reserved assets are rejected atomically; mixed valid/reserved batches must
not partially succeed.
- Item effects, shop prices, cooler capacity, fishing upgrades, jobs, and
experience are separate balance axes. Avoid changing several in an unrelated
presentation pass.
## Recording a balance change
Include in the change description:
1. affected stable IDs and resource paths;
2. old and new values;
3. intended player-facing outcome;
4. interactions with quality, availability, and economy;
5. deterministic validation performed;
6. whether existing saves remain semantically valid.
Do not bump the save schema or network protocol simply because an authored
number changed. Bump a format only when its serialized contract changes.

352
docs/DEVELOPMENT.md Normal file
View file

@ -0,0 +1,352 @@
# Development reference
This is the authoritative reference for NETfishing architecture, engineering
decisions, content policy, authoring, and validation.
## Architecture
### Composition
`main/main.tscn` is the application composition root. It owns long-lived
services, the active world, player container, save/settings managers, and the
pixelated UI viewport. Services are scene-owned rather than global autoloads,
which makes their dependencies visible in the main scene and allows focused
tests to instantiate the same composition.
The main script coordinates title, session, world, and UI lifecycle. Domain
logic remains in typed resources and services rather than being stored solely
in controls.
### Domain boundaries
- `fish/` defines species, availability, catches, pools, quality, and selection.
- `fishing/` owns cast/chase state and resolves authoritative fishing surfaces.
- `inventory/`, `items/`, `economy/`, `progression/`, and `jobs/` own persistent
player-facing game state.
- `world/` owns authored map composition and presentation. Water type is a
shared typed definition used by water bodies, fish habitat, fishing
validation, Logbook classification, and saltwater shoreline presentation.
- `ui/` observes and invokes domain services. It does not define stable item or
fish identity.
Resource files (`.tres`) are authored data. Stable IDs, not display names or
node names, connect persistent and networked state to that data.
### Networking and authority
`NetworkSession` owns ENet session lifecycle and peer registration. Dedicated
network services validate and replicate bounded domains such as fishing,
sales, shops, item use, profiles, jobs, mail, chat, drawings, time, and weather.
`DiscoveryClient` is an optional directory layer beside `NetworkSession`. An
open host may publish a short-lived room lease, and the shared Join Game page
may browse compatible leases before handing the selected address back to the
existing direct ENet connection flow. The directory does not carry gameplay
traffic or become a gameplay authority. Its base URL comes from
`network/discovery/base_url`, with `NETFISHING_DISCOVERY_URL` available as a
development/deployment override.
The host is authoritative. Clients submit requests or evidence; the host
derives trusted context from registered peers, authoritative regions, and
server-owned state before mutating inventory, wallet, progression, or shared
world state. Private single-player uses the same host path. Client evidence is
bounded and reconstructed where possible; UI activation is never proof that a
server-side mutation succeeded.
Moderation follows the same boundary. Player hosts may grant session-scoped
operator status to an authenticated identity. Dedicated servers derive
operators from their configured fingerprint allowlist. Operator status is
replicated for presentation, but kick, ban, unban, and artwork-reset requests
are always reauthorized against the authenticated sender by the host. Only a
player host can grant or revoke operators; operators cannot moderate the host
or another operator.
Protocol compatibility is defined in `network/network_protocol.gd`. A visible
release version is not a reason to change the protocol number.
### Persistence and stable identifiers
`PlayerDataRoot` selects and validates a portable data root. Stores receive
paths from that owner rather than inventing unrelated locations. Progression is
written by `PlayerSaveManager`; device settings and social/identity stores have
separate formats and lifecycles.
Save migrations are sequential and explicit. Existing catches and ownership
are keyed by stable IDs so authored metadata can evolve without rewriting
historical records. Current source constants—not documentation—are
authoritative for format versions.
Repository-owned resources are authoritative authored data. Persistent and
networked records refer to stable IDs; display names, filenames, node names,
coordinates, and pool names are not compatibility identifiers. Cross-domain
concepts such as water type use one typed definition. Removing or changing a
stable ID requires an explicit compatibility plan.
### Presentation
Gameplay is rendered in 3D with the GL Compatibility renderer. The main UI is
presented through a uniformly scaled SubViewport. Shared UI components and
palette resources prevent page-specific geometry and style drift.
World presentation systems (time/weather visuals, procedural sky and water,
shoreline ribbons, player blob shadows) do not own gameplay collision or
network authority. Generated shoreline meshes are deterministic presentation
resources baked from explicitly configured static terrain.
### Engineering consequences
- Shared mutations must behave consistently for private hosts, open hosts,
dedicated servers, and joined clients.
- Networking changes require local-host and paired host/client validation.
- Protocol payloads and rejection cleanup are explicit and bounded.
- Renaming visible UI does not require a save migration.
- Content validation covers stable-ID uniqueness, catalog completeness, pool
membership, and typed habitat compatibility.
- New authored assets require provenance records as well as resource
references.
## Content and balance
Balance is authored data and should change deliberately. Exact current values
remain authoritative in `.tres` resources and typed scripts.
### Fish
- Stable fish IDs and catalog numbers must not be changed for display cleanup.
- Species availability may constrain water type, time, weather, bait, and
other authored context.
- Location pools control selection weight; the global catalog remains
comprehensive.
- Freshwater and saltwater habitat is validated by the host in addition to
pool membership.
- Value, rarity, quality, weight ranges, and authored barrier-health bands
should be reviewed together because they affect economy and catch
difficulty.
### Economy and progression
- Sales and purchases are host-authoritative and must mutate inventory and
wallet exactly once.
- Reserved assets are rejected atomically; mixed valid and reserved batches
must not partially succeed.
- Item effects, shop prices, cooler capacity, fishing upgrades, jobs, and
experience are separate balance axes. Avoid changing several in an
unrelated presentation pass.
Record affected stable IDs and resource paths, old and new values, intended
player-facing outcome, interactions with quality/availability/economy,
deterministic validation, and save-compatibility impact with each balance
change. Do not bump a save schema or network protocol merely because an
authored number changed.
## Authoring
### World composition
The active test world uses the canonical starter island through
`world/regions/starter_island_region.tscn`.
`world/test_world.tscn` owns the gameplay-wide environment, sun, world bounds,
and below-world failsafe. `StarterIslandRegion` owns the placed island content:
```text
StarterIslandRegion
├── Terrain
│ ├── Visual
│ └── Collision
├── WaterBodies
│ ├── Pond
│ │ ├── VisualWater
│ │ ├── FishingRegion
│ │ └── RecoveryRegion
│ └── Ocean
│ ├── VisualWater
│ ├── FishingRegions
│ └── RecoveryRegions
├── PlayerSpawn
├── SafeRespawns
└── Interactables
├── FishingShopWorld
└── PelicanCoolerPerch
```
Move a meaningful feature root rather than one of its implementation children.
Child transforms are local offsets owned by that feature.
Every fishable region authors its `water_type` explicitly. The starter pond is
`FRESH_WATER`; the starter coast is `SALT_WATER`. Fish species use the same
central type through an allowed-water-type bitmask. Do not infer habitat from
node names, pool filenames, coordinates, or water height.
Select `WaterBodies/Pond` to move or resize the pond. Its transform is the
authoritative surface position; surface size and fishing/recovery coverage are
owned together. Select `WaterBodies/Ocean` to move the surrounding water as one
feature. Its explicit visual, fishing, and recovery lobes remain local to that
root.
Placed-feature ownership:
- `PlayerSpawn` defines the initial player transform and facing.
- `SafeRespawns` contains authoritative recovery destinations. Recovery
preserves current facing.
- `Interactables/FishingShopWorld` owns the shop visual, collision, interaction
area, prompt, and entrance marker.
- `Interactables/PelicanCoolerPerch` owns the Pelican landmark visual; selling
remains a Cooler action.
- `Terrain` owns imported visuals and generated collision.
- `BelowWorldFailsafe/Coverage` owns below-world recovery coverage.
The starter-island GLB hierarchy is the terrain-collision authority. The
region rebuilds one concave collision shape from every imported
`MeshInstance3D` when it loads, so newly exported terrain and props participate
without a separate stale collision bake. Keep purely decorative meshes out of
that hierarchy if they should not block players.
The active `Environment`, `Sun`, and starter-island grass material remain
Inspector-authored and are not recreated by runtime scripts.
### Reusable world props
Use one placed `WorldProp` root for a complete landmark:
```text
WorldProp
├── Visual
└── Collision
└── CollisionShape3D
```
Put optional interaction areas, labels, and markers beneath the same root.
Instance imported GLB content under `Visual` and keep gameplay collision under
`Collision`. Author reusable assets at scale `(1, 1, 1)`. Uniform provisional
root scaling is acceptable when visual and collision scale together; avoid
non-uniform root scaling. Make mutable per-instance shapes, meshes, and
materials local to the scene.
### Shoreline tide ribbons
Maps using generated tide ribbons own a `ShorelineRibbonBaker` and one
`ShorelineRibbonConfig` per water body. Each configuration selects only its
static terrain source, water height, generation bounds, water-facing reference,
smoothing overrides, and generated `.tres` path. Props, players, bobbers, and
gameplay areas are not scanned unless selected explicitly. Only `SALT_WATER`
bodies generate ribbons.
The editor automatically rebuilds configured shoreline resources when an
authored terrain GLB is reimported. Select the baker and press **Rebuild
Shoreline Ribbons** only when intentionally forcing a rebuild without a terrain
reimport. The equivalent headless fallback and validation command is:
```sh
godot --headless --path . --script scripts/bake_shoreline_ribbons.gd
```
Use `debug_path_stage` only while comparing extraction stages and leave it Off
in committed scenes. A rebuild is required when saltwater terrain at the
waterline, water height, or generation bounds change; terrain reimports handle
the usual case automatically. Normal gameplay loads committed generated meshes
and never runs extraction.
### Facial-feature textures
Put new PNGs under the matching directory in
`art/exported/characters/faces/`:
```text
eyes/<id>.png
noses/<id>.png
mouth/<id>.png
```
The category directory is authoritative. A category prefix is optional, so
both `sleepy.png` and `eyes_sleepy.png` produce the stable option ID `sleepy`.
Filenames normalize to lowercase snake_case and are stored in appearance
snapshots, so do not casually rename them. Preserve the established RGBA
transparent canvas. Restart a development build after adding an image; rebuild
exports to package new `res://` files.
### Texture sampling
NETfishing artwork always uses nearest-neighbor sampling. Do not enable linear,
bilinear, trilinear, or anisotropic texture filtering, and do not generate
mipmaps. Shader samplers must declare `filter_nearest`. The
`TextureSamplingPolicy` autoload applies nearest sampling to 2D canvas items,
Sprite3D presentations, imported 3D materials, and dynamically constructed
mesh materials at runtime.
After importing new artwork, normalize its tracked import metadata where
needed and run the focused policy check:
```sh
godot --headless --path . --script scripts/normalize_texture_imports.gd \
-- --apply --root res://path/to/new/artwork
godot --headless --path . --script tests/texture_sampling_validation.gd
```
### Bubble menus
Instance `ui/components/bubble_menu/bubble_button.tscn` for each action, or
attach `bubble_button.gd` to an existing button. Author neutral size, desktop
and compact anchors, font-size limits, and deterministic motion in the
Inspector. A label child may be assigned with `label_control_path`.
Place buttons under a Control using `bubble_cluster.gd`, pass ordered button
references to `configure()`, and call `apply_layout()` when available size or
responsive layout changes. Order defines keyboard/controller focus neighbors.
The shared profile owns palette, styles, proportional-font ratio, hover
response, and contact tuning. Parent menus own labels, actions, anchors,
availability, and confirmation behavior.
`motion_scale = 0.0` disables idle drift/deformation while preserving hover and
focus feedback. Contact correction is deterministic and bounded; do not replace
it with physics that destabilizes layout, focus order, or hit targets.
## Validation
Tests are executable Godot `SceneTree` scripts. Some are pure content/domain
checks, some instantiate the main scene, and multiplayer checks run paired host
and client processes on loopback. The repository runner provides isolation and
consistent entry points.
### Consolidated runner
```sh
scripts/run_validations.sh quick
scripts/run_validations.sh full
scripts/run_validations.sh host
scripts/run_validations.sh network
scripts/run_validations.sh all
scripts/run_validations.sh --list
```
- `quick` runs deterministic content and domain checks.
- `full` adds socket-free scene/runtime checks.
- `host` runs single-process authority checks that bind local UDP.
- `network` runs loopback host/client pairs.
- `all` runs `full`, `host`, and `network`.
Set `GODOT_BIN` to select an executable and `TEST_TIMEOUT_SECONDS` to change
the per-process timeout. The runner isolates and removes XDG roots.
For one focused test:
```sh
test_root="$(mktemp -d)"
XDG_DATA_HOME="$test_root/data" \
XDG_CONFIG_HOME="$test_root/config" \
godot --headless --path . --script tests/fish_catalog_content_validation.gd
rm -rf -- "$test_root"
```
Do not point `NETFISHING_DATA_DIR` at an arbitrary empty directory; it is an
explicit portable-data override and must identify a valid data root.
Headless tests cannot prove visual alignment, mouse routing, shader appearance,
controller feel, or resize behavior. Presentation changes require graphical
review at the canonical 1280×720 layout and relevant low/high and ultrawide
resolutions.
Network tests require free loopback UDP ports and local-socket permission. A
release audit additionally requires clean Git/tag verification, editor import,
platform exports, executable smoke tests, archive inspection, and SHA-256
verification; those checks are intentionally outside the development runner.

View file

@ -1,28 +0,0 @@
# PortMaster performance profile
The PortMaster launcher keeps the normal game profile on unknown and stronger
hardware. It enables the low-end profile only when the Linux device tree
reports one of the identifiers used by the Allwinner H616/H700 XX handhelds:
- `allwinner,h616`
- `sun50iw9p1`
- `allwinner,sun50i-h700`
The low-end launch profile passes these options to Godot:
- `--single-window`
- `--disable-vsync`
- `--max-fps 30`
- `--audio-output-latency 40`
It also passes `NETFISHING_LOW_END=1`. The game responds by rendering the 3D
world at 75 percent linear resolution with nearest-neighbor scaling. This
reduces 3D pixel work by about 44 percent while the separately rendered UI
retains its canonical resolution.
Do not use Godot's low processor mode for this profile. It reduces idle CPU
usage by sleeping between updates and is not a game-performance optimization.
When adding another device family, record its exact NUL-separated device-tree
`compatible` value from the hardware before extending the launcher match. Do
not infer detection from a retail product name alone.

View file

@ -1,4 +1,4 @@
# PortMaster release builds
# PortMaster reference
NETfishing's PortMaster release is a canonical PortMaster package, not a
version-named generic ZIP. The installer-facing archive must be named
@ -93,10 +93,14 @@ The remaining catalog metadata assets were copied from the installed official
```text
e2a9d132744684c67865c01eca027a8c9946b4c5a19da57c82f1af0373dc83b7 gameinfo.xml
ff08aacc52bbdc95616320800da3eaee0c0ba5fafdd962bf04f9655859409764 licenses/CREDITS.md
b84fdd2c3da5db56385cdbb639795e90aa3e035c53bc3591135f18df3331451f licenses/Tuffy-LICENSE.txt
```
The packages `licenses/` directory is assembled from the authoritative root
licenses and trademarks, the consolidated `docs/ATTRIBUTION.md`, and the font
notices at build time. It also records the exact release tag and corresponding
GPL source commit. Do not restore stale duplicated notice templates beneath
`scripts/portmaster/`.
The original catalog porter credit, `Voyager`, remains in `port.json`.
## muOS H700 controller mapping
@ -135,3 +139,31 @@ following in the installed game:
- Both sticks, both bumpers, both triggers, Select, Start, L3, and R3 register.
- A single button never cancels auto-map.
- Holding both bumpers together for 1.25 seconds cancels auto-map.
## Low-end performance profile
The launcher keeps the normal game profile on unknown and stronger hardware.
It enables the low-end profile only when the Linux device tree reports one of
the identifiers used by Allwinner H616/H700 XX handhelds:
- `allwinner,h616`
- `sun50iw9p1`
- `allwinner,sun50i-h700`
The low-end profile passes these Godot options:
- `--single-window`
- `--disable-vsync`
- `--max-fps 30`
- `--audio-output-latency 40`
It also passes `NETFISHING_LOW_END=1`. The game renders the 3D world at 75%
linear resolution with nearest-neighbor scaling, reducing 3D pixel work by
about 44% while the separately rendered UI retains its canonical resolution.
Do not use Godot's low processor mode for this profile. It reduces idle CPU
usage by sleeping between updates and is not a game-performance optimization.
Before adding a device family, record its exact NUL-separated device-tree
`compatible` value from hardware. Do not infer detection from a retail product
name alone.

View file

@ -1,12 +1,13 @@
NETfishing
v0.6.2-alpha
Alpha 0.6.2
v0.7.1-alpha
Alpha 0.7.1
Thank you for trying this early private playtest.
NETfishing is currently a local gameplay prototype for a future
multiplayer-first game. Networking and multiplayer are NOT implemented in
this build.
NETfishing is an early multiplayer fishing game. This alpha includes local
progression, player-hosted and dedicated multiplayer sessions, public room
discovery, and direct UDP connections. Expect rough edges and please keep a
backup of any save or identity data you care about.
INSTALLATION
@ -54,12 +55,15 @@ FISHING
- Coffee improves movement; other supplies temporarily improve fishing.
- The Cooler starts at 12 fish; buy permanent capacity expansions at the shop.
- Reel Speed upgrades increase authoritative green reeling progress.
- Barrier Power upgrades increase damage per valid barrier action.
- Rod Power upgrades increase damage per valid barrier action.
- Shop upgrades persist in the local progression save.
- Escape closes shop confirmations before closing the shop itself.
FEATURES TO TRY
- Hosting a private or public room and joining compatible online sessions
- Direct-IP, saved-server, recent-server, and public-room connection flows
- Text chat, player calls, player lists, relationships, and moderation tools
- Movement, jumping, camera rotation, and zoom
- Short, medium, and maximum-distance casts
- Valid water and invalid land casts
@ -101,11 +105,11 @@ Please include:
- How movement, casting, chase pressure, barrier clicking, menus, and camera
controls felt
KNOWN PRE-ALPHA LIMITATIONS
KNOWN ALPHA LIMITATIONS
- Multiplayer/networking is not implemented.
- Fish and character art are temporary.
- UI and balance values are placeholders.
- There is no audio.
- Public sessions remain direct connections; restrictive NAT or carrier-grade
networks may prevent some players from joining.
- Multiplayer, dedicated-server deployment, UI, and balance are still alpha.
- Some fish and character artwork remains temporary.
- There is no installer or automatic updater.
- Windows builds are unsigned.

60
docs/README.md Normal file
View file

@ -0,0 +1,60 @@
# NETfishing documentation
This directory is the single home for maintained project documentation.
Start here instead of searching feature, asset, world, or script directories
for local README files.
## Authoritative references
- [`DEVELOPMENT.md`](DEVELOPMENT.md): architecture, engineering decisions,
content and balance policy, authoring guidance, and validation.
- [`ATTRIBUTION.md`](ATTRIBUTION.md): ownership, credits, licensing map,
third-party notices, asset provenance, and redistribution requirements.
- [`PORTMASTER.md`](PORTMASTER.md): PortMaster packaging, installation,
controller mapping, performance behavior, and hardware release checks.
- [`README-PLAYTEST.txt`](README-PLAYTEST.txt): the player-facing README staged
into desktop playtest packages.
## Related repositories
- [Discovery service API](https://forge.makearmy.io/woofmeow/netfishing-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)
stays with its installer and update scripts. It consumes this repository's
attribution and legal notices from the exact pinned game commit instead of
maintaining copies.
Those code-coupled references remain in their own repositories. Game-wide
architecture, authoring, licensing, credits, and provenance are not duplicated
there.
Exact runtime values and behavior remain authoritative in source code, scenes,
and resources. These documents describe boundaries, policy, and maintenance
procedures; they should not duplicate changing constants or inventories that
can be derived reliably from the project.
## Files intentionally kept at the repository root
The following are entry points or governing legal documents rather than
general project documentation, so they remain at conventional root paths:
- `README.md`
- `CONTRIBUTING.md`
- `LICENSE`
- `ASSET-LICENSE.md`
- `TRADEMARKS.md`
- `CONTRIBUTOR-TERMS.md`
Third-party font license texts stay beside their font files under `ui/fonts/`
so the licensed files and their original notices cannot drift apart. Build
scripts collect those legal files and the authoritative attribution record
when assembling releases.
## Maintenance rule
Do not add feature-specific README files elsewhere in the repository. Add or
update the appropriate reference above and link to its heading when source
comments need more context. Player-facing package instructions belong only in
`README-PLAYTEST.txt`; source and scripts remain authoritative for command-line
usage.

View file

@ -1,63 +0,0 @@
# Testing
NETfishing validations are Godot `SceneTree` scripts. They use assertions and
exit nonzero on failure. Run them with isolated user data so development checks
cannot touch real saves, identity, trust, relationships, or settings.
## Consolidated runner
```sh
scripts/run_validations.sh quick
scripts/run_validations.sh full
scripts/run_validations.sh host
scripts/run_validations.sh network
scripts/run_validations.sh all
scripts/run_validations.sh --list
```
- `quick` runs deterministic content and domain validations.
- `full` adds socket-free scene/runtime checks suitable for headless execution.
- `host` runs single-process authoritative-host checks that bind a local UDP
port.
- `network` runs the loopback host/client validations in pairs.
- `all` runs `full`, `host`, and then `network`.
Set `GODOT_BIN` to select another executable. Set `TEST_TIMEOUT_SECONDS` to
change the per-process timeout. The runner creates one temporary XDG root per
process and removes it on exit.
## One focused test
```sh
test_root="$(mktemp -d)"
XDG_DATA_HOME="$test_root/data" \
XDG_CONFIG_HOME="$test_root/config" \
godot --headless --path . --script tests/fish_catalog_content_validation.gd
rm -rf -- "$test_root"
```
Do not set `NETFISHING_DATA_DIR` to an arbitrary empty folder: that variable is
an explicit portable-data override and must point at a valid NETfishing data
root. Isolating XDG paths is sufficient for the validation scripts.
## Graphical checks
Headless tests cannot prove visual alignment, actual mouse routing, shader
appearance, controller feel, or window-resize behavior. Presentation changes
need a real graphical startup with isolated XDG roots and inspection at the
canonical 1280×720 layout plus relevant low/high and ultrawide resolutions.
## Network checks
Host and paired tests bind loopback UDP ports defined in their scripts. Ensure
no old Godot validation process is holding those ports and that the execution
environment permits local sockets. The runner starts paired hosts before their
clients and requires both processes to exit successfully. Network tests do not
contact external servers.
## Release checks
A release audit additionally includes clean Git/tag verification, editor import,
export-template checks, platform exports, executable smoke tests, archive
inspection, and SHA-256 verification. Those checks are intentionally not hidden
inside the development runner.

View file

@ -1,27 +0,0 @@
# ADR 0001: Host-authoritative shared state
Status: accepted
## Context
NETfishing supports private hosts, open hosts, and joined clients. Catches,
currency, owned items, jobs, mail, drawings, profiles, and moderation affect
state that cannot safely trust a client's local presentation.
## Decision
The host validates requests against registered peers and host-owned context,
then performs the authoritative mutation. Clients receive results and replicated
presentation state. Client evidence is bounded and reconstructed where
possible; it is not treated as authority.
Private single-player uses the same host path. Presentation-only state does not
add RPCs or persistence.
## Consequences
- Shared behavior is consistent across private, open-host, and joined-client
modes.
- Tests must cover local-host and paired host/client paths.
- Stable protocol payloads and rejection cleanup require explicit handling.
- UI activation cannot be used as proof that a server-side mutation succeeded.

View file

@ -1,27 +0,0 @@
# ADR 0002: Authored resources and stable identifiers
Status: accepted
## Context
Fish, items, pools, availability, shop stock, and other game content evolve
independently from saved catches and network messages. Display labels and scene
node names are expected to change during development.
## Decision
Repository-owned resources are the authoritative authored data. Persistent and
networked records refer to stable IDs. Display names, filenames, node names,
coordinates, and pool names are not compatibility identifiers.
Cross-domain concepts such as water type use one typed definition rather than
duplicated strings. Comprehensive catalogs remain separate from location-
specific selection pools.
## Consequences
- Renaming visible UI does not require save migration.
- Removing or changing a stable ID requires an explicit compatibility plan.
- Content validation checks uniqueness, catalog completeness, pool membership,
and typed habitat compatibility.
- New authored assets need provenance records as well as resource references.

View file

@ -340,13 +340,13 @@ func _sample_cell_transform(
tangent = Vector3.RIGHT
var bitangent: Vector3 = normal.cross(tangent).normalized()
var pixel_size: float = cell_size * clampf(fill, 0.05, 1.0)
var basis := Basis(
var cell_basis := Basis(
tangent * pixel_size,
bitangent * pixel_size,
normal,
)
return Transform3D(
basis,
cell_basis,
to_local(point + normal * _surface_offset()),
)

View file

@ -9,4 +9,4 @@ display_name = "pelicans"
animal_name_singular = "pelican"
animal_name_plural = "pelicans"
payout_multiplier = 0.25
sale_message_template = "You sold your {fish} to the pelicans for ${payout}."
sale_message_template = "You sold your {fish} to the pelicans."

View file

@ -53,8 +53,7 @@ func get_sale_message(
var buyer_name: String = animal_name_plural
if buyer_name.is_empty():
buyer_name = display_name
return "you sold your %s to the %s for $%d." % [
return "you sold your %s to the %s." % [
fish_name,
buyer_name,
payout,
]

View file

@ -165,9 +165,10 @@ func _validate_batch(
result.payout
)
else:
result.sale_message = "you sold %d fish to the %s for $%d." % [
result.sale_message = (
"you sold %d fish to the %s." % [
result.fish_count,
result.buyer_animal_name,
result.payout,
]
)
return result

View file

@ -6,26 +6,30 @@ const ItemDataType = preload("res://items/item_data.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
const WORM_MAX_STACK: int = 10
const FISH_FINDER_ID: StringName = &"fish_finder"
const BATTERIES_ID: StringName = &"batteries"
const ITEM_PRICES: Dictionary[StringName, int] = {
&"worms": 1,
&"snails": 3,
&"shrimp": 7,
&"squid_chunks": 15,
&"whole_anchovy": 30,
&"whole_sardine": 60,
&"whole_anchovy": 60,
&"whole_sardine": 30,
&"luminous_roe": 125,
&"the_standby": 750,
&"coffee": 20,
&"energy_drink": 35,
&"snack": 30,
&"fish_finder": 60,
FISH_FINDER_ID: 500,
BATTERIES_ID: 15,
}
const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = {
&"snails": 400,
&"shrimp": 1200,
&"squid_chunks": 3500,
&"whole_anchovy": 9000,
&"whole_sardine": 22000,
&"whole_anchovy": 22000,
&"whole_sardine": 9000,
&"luminous_roe": 50000,
}
const ITEM_ORDER: Array[StringName] = [
@ -33,13 +37,15 @@ const ITEM_ORDER: Array[StringName] = [
&"snails",
&"shrimp",
&"squid_chunks",
&"whole_anchovy",
&"whole_sardine",
&"whole_anchovy",
&"luminous_roe",
&"the_standby",
FISH_FINDER_ID,
&"coffee",
&"energy_drink",
&"snack",
&"fish_finder",
BATTERIES_ID,
]
@ -69,6 +75,20 @@ static func is_bait_topoff(item_id: StringName) -> bool:
return item_id == &"worms" or BAIT_UNLOCK_PRICES.has(item_id)
static func is_permanent_unlock(
item_id: StringName,
item: ItemDataType,
) -> bool:
return (
item != null
and (item.is_lure() or item_id == FISH_FINDER_ID)
)
static func is_passive_supply(item_id: StringName) -> bool:
return item_id == BATTERIES_ID
static func get_purchase_cost(
item_id: StringName,
quantity: int,
@ -92,6 +112,7 @@ static func purchase_one(
return false
var item: ItemDataType = catalog.get_item_by_id(item_id)
var bait_topoff: bool = is_bait_topoff(item_id)
var permanent_unlock: bool = is_permanent_unlock(item_id, item)
var quantity: int = get_purchase_quantity(
item_id,
bag.get_quantity(item_id),
@ -106,9 +127,18 @@ static func purchase_one(
price < 0
or item == null
or not item.is_valid()
or (item.category != ItemDataType.Category.CONSUMABLE and not bait_topoff)
or not item.stackable
or (not item.usable and not bait_topoff)
or (
item.category != ItemDataType.Category.CONSUMABLE
and not bait_topoff
and not permanent_unlock
)
or (not item.stackable and not permanent_unlock)
or (
not item.usable
and not bait_topoff
and not permanent_unlock
and not is_passive_supply(item_id)
)
or not bag.can_add_item(item_id, quantity)
or not wallet.can_afford(price)
):

View file

@ -7,9 +7,9 @@ advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.2-alpha/windows-x86_64/NETfishing.exe"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*"
export_path="builds/v0.7.1-alpha/windows-x86_64/NETfishing.exe"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -35,13 +35,13 @@ application/modify_resources=true
application/icon="res://art/exported/system_icons/netfishing.ico"
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version="0.6.2.0"
application/product_version="0.6.2.0"
application/company_name=""
application/file_version="0.7.1.0"
application/product_version="0.7.1.0"
application/company_name="Woofmeow"
application/product_name="NETfishing"
application/file_description="NETfishing v0.6.2-alpha"
application/copyright=""
application/trademarks=""
application/file_description="NETfishing v0.7.1-alpha"
application/copyright="Copyright © 2026 Woofmeow"
application/trademarks="NETfishing and Woofmeow branding is reserved; see TRADEMARKS.md"
application/export_angle=0
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
@ -60,9 +60,9 @@ advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.2-alpha/linux-arm64/NETfishing.arm64"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*"
export_path="builds/v0.7.1-alpha/linux-arm64/NETfishing.arm64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -90,9 +90,9 @@ advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.2-alpha/macos/NETfishing.zip"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*"
export_path="builds/v0.7.1-alpha/macos/NETfishing.zip"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -108,12 +108,42 @@ custom_template/release=""
application/bundle_identifier="io.woofmeow.netfishing"
application/icon="res://art/exported/system_icons/netfishing_1024.png"
application/icon_interpolation=0
application/short_version="0.6.2"
application/version="0.6.2"
application/short_version="0.7.1"
application/version="0.7.1"
application/architecture="universal"
codesign/enable=false
notarization/enable=false
[preset.5]
name="Linux Dedicated Server"
platform="Linux"
runnable=false
advanced_options=false
dedicated_server=true
custom_features=""
export_filter="all_resources"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*,tests/*"
export_path="builds/v0.7.1-alpha/server-linux-x86_64/NETfishingServer.x86_64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.5.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=0
binary_format/embed_pck=false
binary_format/architecture="x86_64"
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
[preset.2]
name="Android"
@ -123,9 +153,9 @@ advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.2-alpha/android/NETfishing.apk"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*"
export_path="builds/v0.7.1-alpha/android/NETfishing.apk"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -146,8 +176,8 @@ architectures/armeabi-v7a=false
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
version/code=60200
version/name="v0.6.2-alpha"
version/code=70100
version/name="v0.7.1-alpha"
package/unique_name="io.woofmeow.netfishing"
package/name="NETfishing"
package/signed=true
@ -182,9 +212,9 @@ advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.2-alpha/linux-x86_64/NETfishing.x86_64"
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
exclude_filter="builds/*,scripts/*"
export_path="builds/v0.7.1-alpha/linux-x86_64/NETfishing.x86_64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""

View file

@ -19,20 +19,15 @@ const WORM_ROLL_WEIGHTS: Array[float] = [80.0, 10.0, 5.0, 4.0, 1.0]
const SNAIL_ROLL_WEIGHTS: Array[float] = [68.0, 16.0, 8.0, 6.0, 2.0]
const SHRIMP_ROLL_WEIGHTS: Array[float] = [56.0, 21.0, 12.0, 8.0, 3.0]
const SQUID_ROLL_WEIGHTS: Array[float] = [44.0, 25.0, 16.0, 11.0, 4.0]
const ANCHOVY_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0]
const MINNOW_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0]
const SARDINE_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0]
const LUMINOUS_ROE_ROLL_WEIGHTS: Array[float] = [16.0, 26.0, 26.0, 20.0, 12.0]
const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0]
# Legacy profile multiplier retained for serialized/profile compatibility.
# New encounters use the weighted quality/rarity/weight bands below.
# Legacy multiplier helpers remain available for compatibility. New encounters
# roll directly within the authored quality bands below.
const BARRIER_HEALTH_MULTIPLIERS: Array[float] = [1.0, 1.25, 1.6, 2.2, 3.25]
# Barrier health is weighted 70% by quality, 20% by rarity, and 10% by the
# catch's position in its authored weight range. The upper bounds define the
# intended per-barrier challenge bands; a small seeded variance keeps barriers
# from feeling identical without crossing those bands.
const BARRIER_QUALITY_WEIGHT: float = 0.70
const BARRIER_RARITY_WEIGHT: float = 0.20
const BARRIER_WEIGHT_WEIGHT: float = 0.10
# Inclusive per-barrier health bands authored for each quality tier. Encounter
# generation rolls uniformly across the selected tier's complete band.
const BARRIER_HEALTH_MINIMUMS: Array[int] = [1, 10, 50, 100, 200]
const BARRIER_HEALTH_MAXIMUMS: Array[int] = [9, 50, 100, 200, 400]
const DISPLAY_NAMES: PackedStringArray = [
@ -89,32 +84,26 @@ static func apply_barrier_health(base_health: int, quality: int) -> int:
)
static func barrier_health_for_catch(
quality: int,
rarity: int,
weight_percentile: float,
variance: float = 1.0,
) -> int:
static func barrier_health_range(quality: int) -> Vector2i:
var safe_quality: int = (
quality if is_valid(quality) else Tier.BORING
)
var safe_rarity: float = clampf(float(rarity) / 4.0, 0.0, 1.0)
var safe_weight: float = clampf(weight_percentile, 0.0, 1.0)
var weighted_health: float = float(BARRIER_HEALTH_MAXIMUMS[safe_quality]) * (
BARRIER_QUALITY_WEIGHT
+ BARRIER_RARITY_WEIGHT * safe_rarity
+ BARRIER_WEIGHT_WEIGHT * safe_weight
)
var varied_health: int = roundi(
weighted_health * clampf(variance, 0.8, 1.2)
)
return clampi(
varied_health,
return Vector2i(
BARRIER_HEALTH_MINIMUMS[safe_quality],
BARRIER_HEALTH_MAXIMUMS[safe_quality],
)
static func roll_barrier_health(
rng: RandomNumberGenerator,
quality: int,
) -> int:
var health_range: Vector2i = barrier_health_range(quality)
if rng == null:
return health_range.x
return rng.randi_range(health_range.x, health_range.y)
static func roll(
rng: RandomNumberGenerator,
weight_multipliers: Array[float] = [],
@ -160,7 +149,7 @@ static func rarity_weights_for_bait(
elif active_bait_tags.has(&"bait_rarity_5"):
weights.assign(SARDINE_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_4"):
weights.assign(ANCHOVY_ROLL_WEIGHTS)
weights.assign(MINNOW_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_3"):
weights.assign(SQUID_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_2"):

View file

@ -6,8 +6,6 @@
script = ExtResource("1_profile")
barrier_count_min = 3
barrier_count_max = 4
barrier_health_min = 4
barrier_health_max = 7
first_barrier_margin = 0.14
final_barrier_margin = 0.1
minimum_barrier_spacing = 0.14

View file

@ -6,8 +6,6 @@
script = ExtResource("1_profile")
barrier_count_min = 3
barrier_count_max = 4
barrier_health_min = 5
barrier_health_max = 8
first_barrier_margin = 0.13
final_barrier_margin = 0.09
minimum_barrier_spacing = 0.14

View file

@ -69,8 +69,6 @@ var _reel_input_held: bool = false
var _reel_speed: float = 0.0
var _click_power: int = 1
var _fish_quality: int = FishQualityType.Tier.BORING
var _fish_rarity: int = 0
var _fish_weight_percentile: float = 0.0
var _chase_delay_remaining: float = 0.0
var _failure_epsilon: float = 0.0001
var _auto_click_accumulator: float = 0.0
@ -113,8 +111,6 @@ func start_encounter(
reel_speed: float,
click_power: int,
fish_quality: int = FishQualityType.Tier.BORING,
fish_rarity: int = 0,
fish_weight_percentile: float = 0.0,
) -> void:
reset()
if profile == null:
@ -128,8 +124,6 @@ func start_encounter(
if FishQualityType.is_valid(fish_quality)
else FishQualityType.Tier.BORING
)
_fish_rarity = clampi(fish_rarity, 0, 4)
_fish_weight_percentile = clampf(fish_weight_percentile, 0.0, 1.0)
_chase_delay_remaining = CHASE_START_DELAY
chase_progress = -CHASE_START_OFFSET
_seed_encounter_rng()
@ -143,22 +137,18 @@ func start_authoritative_encounter(
profile: CatchDifficultyProfileType,
reel_speed: float,
click_power: int,
seed: int,
encounter_seed_value: int,
fish_quality: int = FishQualityType.Tier.BORING,
fish_rarity: int = 0,
fish_weight_percentile: float = 0.0,
) -> void:
var previous_test_mode: bool = use_deterministic_test_seed
var previous_seed: int = deterministic_test_seed
use_deterministic_test_seed = true
deterministic_test_seed = seed
deterministic_test_seed = encounter_seed_value
start_encounter(
profile,
reel_speed,
click_power,
fish_quality,
fish_rarity,
fish_weight_percentile,
)
use_deterministic_test_seed = previous_test_mode
deterministic_test_seed = previous_seed
@ -327,11 +317,8 @@ func _generate_barriers(profile: CatchDifficultyProfileType) -> void:
+ spacing * float(barrier_index)
+ random_offsets[barrier_index] * random_slack
)
var health: int = FishQualityType.barrier_health_for_catch(
_fish_quality,
_fish_rarity,
_fish_weight_percentile,
_rng.randf_range(0.9, 1.1),
var health: int = FishQualityType.roll_barrier_health(
_rng, _fish_quality
)
_barriers.append(Barrier.new(position, health))

View file

@ -3,8 +3,6 @@ extends Resource
@export_range(0, 12, 1) var barrier_count_min: int = 2
@export_range(0, 12, 1) var barrier_count_max: int = 4
@export_range(1, 100, 1) var barrier_health_min: int = 3
@export_range(1, 100, 1) var barrier_health_max: int = 7
@export_range(0.0, 0.45, 0.01) var first_barrier_margin: float = 0.15
@export_range(0.0, 0.45, 0.01) var final_barrier_margin: float = 0.10
@export_range(0.01, 1.0, 0.01) var minimum_barrier_spacing: float = 0.15
@ -15,11 +13,6 @@ func get_barrier_count_range() -> Vector2i:
return Vector2i(minimum, maxi(minimum, barrier_count_max))
func get_barrier_health_range() -> Vector2i:
var minimum: int = maxi(1, barrier_health_min)
return Vector2i(minimum, maxi(minimum, barrier_health_max))
func get_generation_interval() -> Vector2:
var start: float = clampf(first_barrier_margin, 0.0, 0.95)
var finish: float = clampf(1.0 - final_barrier_margin, start, 1.0)

View file

@ -6,8 +6,6 @@
script = ExtResource("1_profile")
barrier_count_min = 2
barrier_count_max = 4
barrier_health_min = 3
barrier_health_max = 7
first_barrier_margin = 0.15
final_barrier_margin = 0.1
minimum_barrier_spacing = 0.15

View file

@ -29,6 +29,7 @@ enum LineMode {
@export_range(0.0, 3.0, 0.01) var maximum_slack: float = 0.65
@export_range(2, 20, 1) var sag_interpolation_count: int = 8
@export_range(0.1, 30.0, 0.1) var line_transition_speed: float = 7.0
@export_range(0.002, 0.05, 0.001) var line_thickness: float = 0.016
@export_category("Cast")
@export_range(0.1, 3.0, 0.05) var cast_travel_duration: float = 0.65
@ -56,6 +57,7 @@ var _rod: Node3D
var _rod_tip: Marker3D
var _rod_neutral_rotation: Vector3
var _cast_arrival_position: Vector3
var _active_cast_arc_height: float = 0.0
var _cast_tracks_water_surface: bool = false
var _bobber_surface_position: Vector3
var _bobber_idle_elapsed: float = 0.0
@ -153,6 +155,7 @@ func begin_cast(
target: Vector3,
blocked_landing_position: Vector3 = Vector3.ZERO,
cast_is_blocked: bool = false,
resolved_arc_height: float = -1.0,
) -> void:
if _mode != VisualMode.AIMING or _rod_tip == null:
return
@ -165,6 +168,11 @@ func begin_cast(
_cast_arrival_position = (
blocked_landing_position if cast_is_blocked else target
)
_active_cast_arc_height = (
maxf(resolved_arc_height, 0.0)
if resolved_arc_height >= 0.0
else cast_arc_height
)
_cast_tracks_water_surface = not cast_is_blocked
var cast_start: Vector3 = _rod_tip.global_position
@ -302,6 +310,7 @@ func cleanup() -> void:
_bobber_base_scale = Vector3.ONE
_bobber.rotation = Vector3.ZERO
_cast_arrival_position = Vector3.ZERO
_active_cast_arc_height = 0.0
_cast_tracks_water_surface = false
_bobber_surface_position = Vector3.ZERO
_bobber_idle_elapsed = 0.0
@ -319,7 +328,7 @@ func _set_cast_sample(
target: Vector3,
) -> void:
var sample: Vector3 = start.lerp(target, progress)
sample.y += sin(progress * PI) * cast_arc_height
sample.y += sin(progress * PI) * _active_cast_arc_height
if _cast_tracks_water_surface:
sample.y += (
WaterSurfaceMotionType.get_default_height_offset()
@ -409,10 +418,51 @@ func _update_line(delta: float) -> void:
_line_mesh.clear_surfaces()
if rendered_points.size() < 2:
return
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINES)
for point_index: int in range(1, rendered_points.size()):
_line_mesh.surface_add_vertex(to_local(rendered_points[point_index - 1]))
_line_mesh.surface_add_vertex(to_local(rendered_points[point_index]))
_draw_line_ribbon(rendered_points)
func _draw_line_ribbon(points: PackedVector3Array) -> void:
var camera: Camera3D = get_viewport().get_camera_3d()
if camera == null:
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP)
for point: Vector3 in points:
_line_mesh.surface_add_vertex(to_local(point))
_line_mesh.surface_end()
return
var half_thickness: float = line_thickness * 0.5
var ribbon_vertices := PackedVector3Array()
for point_index: int in range(1, points.size()):
var start: Vector3 = points[point_index - 1]
var end: Vector3 = points[point_index]
var segment: Vector3 = end - start
if segment.length_squared() <= 0.000001:
continue
var to_camera: Vector3 = (
camera.global_position - start.lerp(end, 0.5)
)
var side: Vector3 = segment.normalized().cross(to_camera.normalized())
if side.length_squared() <= 0.000001:
side = segment.normalized().cross(Vector3.UP)
if side.length_squared() <= 0.000001:
side = segment.normalized().cross(Vector3.RIGHT)
side = side.normalized() * half_thickness
var start_left: Vector3 = to_local(start - side)
var start_right: Vector3 = to_local(start + side)
var end_right: Vector3 = to_local(end + side)
var end_left: Vector3 = to_local(end - side)
ribbon_vertices.append(start_left)
ribbon_vertices.append(start_right)
ribbon_vertices.append(end_right)
ribbon_vertices.append(start_left)
ribbon_vertices.append(end_right)
ribbon_vertices.append(end_left)
if ribbon_vertices.is_empty():
return
_line_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
for vertex: Vector3 in ribbon_vertices:
_line_mesh.surface_add_vertex(vertex)
_line_mesh.surface_end()

View file

@ -47,6 +47,7 @@ const WorldWeatherServiceType = preload(
)
signal status_changed(status: String)
signal local_speech_requested(message: String)
signal catch_display_changed(
progress: float,
chase_progress: float,
@ -64,9 +65,8 @@ signal showcase_changed(
visible: bool,
)
signal bite_activated
signal bite_prompt_changed(is_visible: bool)
signal ready_for_equipment_refresh
signal fish_showcase_toggle_requested
signal art_ui_toggle_requested
enum FishingState {
READY,
@ -151,6 +151,7 @@ var _cast_charge: float = 0.0
var _cast_direction: Vector3 = Vector3.FORWARD
var _cast_origin_position: Vector3
var _cast_target: Vector3
var _preferred_cast_arc_height: float = -1.0
var _withdrawal_endpoint: Vector3
var _withdrawal_progress: float = 0.0
var _withdrawal_input_held: bool = false
@ -175,6 +176,8 @@ var _network_auto_click_accumulator: float = 0.0
var _network_active_barrier_index: int = -1
var _bite_rng: RandomNumberGenerator = RandomNumberGenerator.new()
var _pending_cleanup_message: String = ""
var _bite_confirmation_pending: bool = false
var _bite_confirmation_requested: bool = false
func _ready() -> void:
@ -251,6 +254,9 @@ func setup(
_network_fishing.local_bite_started.connect(
_on_network_bite_started
)
_network_fishing.local_bite_pending.connect(
_on_network_bite_pending
)
_network_fishing.local_snapshot_received.connect(
_on_network_fishing_snapshot
)
@ -355,15 +361,15 @@ func handle_escape_action() -> bool:
func set_local_menu_input_suppressed(
owner: StringName,
input_owner: StringName,
suppressed: bool,
) -> void:
if owner.is_empty():
if input_owner.is_empty():
return
if suppressed:
_local_menu_input_owners[owner] = true
_local_menu_input_owners[input_owner] = true
else:
_local_menu_input_owners.erase(owner)
_local_menu_input_owners.erase(input_owner)
if suppressed and state == FishingState.WAITING_FOR_BITE:
_withdrawal_input_held = false
_stop_reeling_audio()
@ -395,6 +401,11 @@ func begin_water_recovery() -> void:
FishingState.FIGHTING,
]:
_cancel_attempt()
# Recovery replaces the normal rod-return presentation. Finish the local
# cleanup now so its temporary movement lock is not captured and restored
# after the player respawns.
if state != FishingState.READY:
_finalize_attempt_cleanup("")
elif state == FishingState.SHOWING_CATCH:
_secure_showcase_catch_for_recovery()
@ -497,31 +508,28 @@ func _unhandled_input(event: InputEvent) -> void:
FishingState.READY:
if not _new_cast_press_armed:
return
if (
_local_hotbar != null
and not _local_hotbar.get_selected_fish_catch_id().is_empty()
):
fish_showcase_toggle_requested.emit()
get_viewport().set_input_as_handled()
return
var active_item: ItemDataType = _get_active_item()
if (
active_item != null
and active_item.item_id == ArtShopStockType.ART_KIT_ITEM_ID
):
art_ui_toggle_requested.emit()
get_viewport().set_input_as_handled()
return
if (
active_item != null
and active_item.category == ItemDataType.Category.CONSUMABLE
and (
active_item.category == ItemDataType.Category.CONSUMABLE
or (
active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID
)
)
):
if _network_item_use != null:
_network_item_use.request_use(active_item.item_id)
elif _item_effects.use_consumable(active_item, _local_bag):
elif _item_effects.use_item(active_item, _local_bag):
status_changed.emit(_item_effects.get_feedback(
active_item.item_id
))
elif (
active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID
):
local_speech_requested.emit(
PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE
)
get_viewport().set_input_as_handled()
return
if not has_active_fishing_rod():
@ -552,6 +560,10 @@ func _unhandled_input(event: InputEvent) -> void:
else:
return
FishingState.WAITING_FOR_BITE:
if _bite_confirmation_pending:
confirm_pending_bite()
get_viewport().set_input_as_handled()
return
_withdrawal_input_held = true
_start_reeling_audio()
if _network_fishing != null:
@ -568,6 +580,9 @@ func _unhandled_input(event: InputEvent) -> void:
_confirm_cast()
get_viewport().set_input_as_handled()
elif state == FishingState.WAITING_FOR_BITE:
if _bite_confirmation_pending:
get_viewport().set_input_as_handled()
return
_withdrawal_input_held = false
_stop_reeling_audio()
if _network_fishing != null:
@ -596,8 +611,12 @@ func _begin_aiming(player: PlayerType) -> void:
_cast_charge = 0.0
_cast_target = _calculate_cast_target(minimum_cast_distance)
state = FishingState.AIMING_CAST
_active_player.set_casting_visual()
status_changed.emit("")
_cast_path_is_clear = is_cast_path_clear(_cast_origin_position, _cast_target)
_cast_path_is_clear = is_cast_path_clear(
_get_cast_launch_position(),
_cast_target,
)
var target_is_fishable: bool = (
_aim_surface_sample.is_fishable() and _cast_path_is_clear
)
@ -682,6 +701,28 @@ func is_fighting() -> bool:
return state == FishingState.FIGHTING
func confirm_pending_bite() -> void:
if state != FishingState.WAITING_FOR_BITE or not _bite_confirmation_pending:
return
_bite_confirmation_requested = true
if _network_fishing != null:
_network_primary_input_held = false
_network_input_resend_elapsed = 0.0
_network_fishing.submit_local_input(false, true)
else:
_activate_bite(true)
func _set_bite_confirmation_pending(is_pending: bool) -> void:
if _bite_confirmation_pending == is_pending:
if not is_pending:
_bite_confirmation_requested = false
return
_bite_confirmation_pending = is_pending
_bite_confirmation_requested = false
bite_prompt_changed.emit(is_pending)
func is_returning() -> bool:
return state == FishingState.RETURNING
@ -703,7 +744,10 @@ func _update_cast_charge(delta: float) -> void:
var maximum_distance: float = maxf(minimum_cast_distance, maximum_cast_distance)
var distance: float = lerpf(minimum_cast_distance, maximum_distance, _cast_charge)
_cast_target = _calculate_cast_target(distance)
_cast_path_is_clear = is_cast_path_clear(_cast_origin_position, _cast_target)
_cast_path_is_clear = is_cast_path_clear(
_get_cast_launch_position(),
_cast_target,
)
var target_is_fishable: bool = (
_aim_surface_sample.is_fishable() and _cast_path_is_clear
)
@ -723,29 +767,45 @@ func _confirm_cast() -> void:
if state != FishingState.AIMING_CAST:
return
_cast_path_is_clear = is_cast_path_clear(
_cast_origin_position,
var cast_launch_position := _get_cast_launch_position()
var resolved_cast_arc: Dictionary = _surface_resolver.resolve_cast_arc(
get_world_3d().direct_space_state,
cast_launch_position,
_cast_target,
_presentation.cast_arc_height,
)
var resolved_arc_height: float = float(
resolved_cast_arc.get("arc_height", _presentation.cast_arc_height)
)
var resolved_cast_collision: Dictionary = resolved_cast_arc.get(
"collision",
{},
)
_cast_path_is_clear = resolved_cast_collision.is_empty()
var cast_is_invalid: bool = (
not _aim_surface_sample.is_fishable() or not _cast_path_is_clear
)
var arrival_position: Vector3 = _cast_target
if not _cast_path_is_clear:
arrival_position = _resolve_cast_impact_position(
_cast_origin_position,
var collision_position: Variant = resolved_cast_collision.get(
"position",
_cast_target,
)
if typeof(collision_position) == TYPE_VECTOR3:
arrival_position = collision_position
if _active_player != null:
# Movement is available while aiming, then locks once the cast is
# released so the active bobber/fishing event remains anchored.
_active_player.set_movement_enabled(false)
state = FishingState.CASTING
if _active_player != null:
_active_player.set_release_visual()
status_changed.emit("")
_presentation.begin_cast(
_cast_target,
arrival_position,
cast_is_invalid,
resolved_arc_height,
)
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
@ -791,6 +851,7 @@ func _on_cast_completed() -> void:
_consume_active_bait()
state = FishingState.WAITING_FOR_BITE
_active_player.set_fishing_visual(true)
_state_time_remaining = roll_bite_wait_time() * (
_item_effects.get_bite_time_multiplier()
if _item_effects != null
@ -807,7 +868,7 @@ func _on_cast_completed() -> void:
_cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance
)
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
status_changed.emit("waiting for a bite...")
status_changed.emit("")
func _consume_active_bait() -> void:
@ -845,6 +906,8 @@ func roll_bite_wait_time() -> float:
func _update_waiting_for_bite(delta: float) -> void:
if _bite_confirmation_pending:
return
if (
_withdrawal_input_held
and not Input.is_action_pressed("fish_primary")
@ -939,7 +1002,7 @@ func _get_withdrawable_distance() -> float:
return landing_offset.length() - withdrawal_cancel_distance
func _activate_bite() -> void:
func _activate_bite(confirmation_override: bool = false) -> void:
if state != FishingState.WAITING_FOR_BITE:
return
@ -951,7 +1014,19 @@ func _activate_bite() -> void:
):
_cancel_attempt()
return
if (
not confirmation_override
and _active_lure_has_effect(&"deferred_fight")
):
_state_time_remaining = 0.0
_withdrawal_input_held = false
_set_bite_confirmation_pending(true)
status_changed.emit("")
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
_presentation.show_bite()
return
_set_bite_confirmation_pending(false)
state = FishingState.FIGHTING
_active_player.set_fighting_visual(true)
_state_time_remaining = 0.0
@ -970,8 +1045,6 @@ func _activate_bite() -> void:
_get_effective_reel_speed(),
_get_effective_barrier_damage(),
_pending_catch.quality,
int(_pending_catch.fish.rarity),
_pending_catch.fish.get_weight_percentile(_pending_catch.weight_lb),
)
_catch_controller.set_reel_input(
Input.is_action_pressed("fish_primary")
@ -1014,7 +1087,7 @@ func _on_catch_encounter_updated(
barrier_health: PackedInt32Array,
barrier_max_health: PackedInt32Array,
active_barrier_index: int,
visible: bool,
encounter_visible: bool,
) -> void:
catch_display_changed.emit(
progress,
@ -1023,9 +1096,9 @@ func _on_catch_encounter_updated(
barrier_health,
barrier_max_health,
active_barrier_index,
visible
encounter_visible
)
if state != FishingState.FIGHTING or not visible:
if state != FishingState.FIGHTING or not encounter_visible:
return
@ -1043,6 +1116,7 @@ func _on_catch_completed() -> void:
return
_stop_fight_audio()
_active_player.set_fighting_visual(false)
_active_player.set_retract_visual()
state = FishingState.SHOWING_CATCH
_showcase_ready = false
_showcase_outcome_completed = false
@ -1057,13 +1131,16 @@ func _on_catch_escaped() -> void:
return
_stop_fight_audio()
var fish_name: String = "the fish"
if _selected_fish != null and not _selected_fish.display_name.is_empty():
fish_name = "the %s" % _selected_fish.display_name
_cleanup_attempt("%s got away!" % fish_name, &"escape")
_cleanup_attempt("", &"escape")
func _on_outcome_completed(outcome: StringName) -> void:
var retracting_player := _active_player
if (
retracting_player != null
and not retracting_player.is_retract_visual_complete()
):
await retracting_player.retract_visual_finished
if state == FishingState.RETURNING:
var cleanup_message: String = _pending_cleanup_message
_pending_cleanup_message = ""
@ -1163,10 +1240,20 @@ func _cleanup_attempt(
cooldown_message: String = "",
visual_outcome: StringName = &"",
) -> void:
_set_bite_confirmation_pending(false)
_stop_fight_audio()
_stop_reeling_audio()
if _active_player != null:
_active_player.set_fighting_visual(false)
if visual_outcome.is_empty():
_active_player.set_fishing_visual(false)
elif visual_outcome == &"catch":
# A successful catch already retracted the line before the fish
# showcase. Returning from the pocket transition should settle back
# to idle instead of replaying the retract animation.
_active_player.set_fishing_visual(false)
else:
_active_player.set_retract_visual()
if not visual_outcome.is_empty():
if state == FishingState.RETURNING:
return
@ -1196,6 +1283,7 @@ func _finalize_attempt_cleanup(cooldown_message: String) -> void:
_showcase_restore_generation += 1
if _active_player != null:
_active_player.set_fighting_visual(false)
_active_player.set_fishing_visual(false)
_active_player.end_catch_showcase()
_active_player.set_movement_enabled(true)
_active_player = null
@ -1278,6 +1366,14 @@ func _capture_cast_direction(player: PlayerType) -> Vector3:
return direction.normalized()
func _get_cast_launch_position() -> Vector3:
if _active_player != null:
var rod_tip: Marker3D = _active_player.get_fishing_rod_tip()
if rod_tip != null and is_instance_valid(rod_tip):
return rod_tip.global_position
return _cast_origin_position
func _calculate_cast_target(distance: float) -> Vector3:
var query_position := Vector3(
_cast_origin_position.x + _cast_direction.x * distance,
@ -1323,6 +1419,7 @@ func resolve_fishing_surface(
get_world_3d().direct_space_state,
target,
resolved_reference_y,
water_occlusion_tolerance,
)
@ -1333,17 +1430,25 @@ func is_target_fishable(target: Vector3) -> bool:
func _is_cast_target_valid(target: Vector3) -> bool:
return (
is_target_fishable(target)
and is_cast_path_clear(_cast_origin_position, target)
and is_cast_path_clear(_get_cast_launch_position(), target)
)
func is_cast_path_clear(origin: Vector3, target: Vector3) -> bool:
return _surface_resolver.find_first_cast_collision(
if _preferred_cast_arc_height < 0.0:
_preferred_cast_arc_height = maxf(
_presentation.cast_arc_height,
0.0,
)
var resolved_arc: Dictionary = _surface_resolver.resolve_cast_arc(
get_world_3d().direct_space_state,
origin,
target,
_presentation.cast_arc_height,
).is_empty()
_preferred_cast_arc_height,
)
_presentation.cast_arc_height = float(resolved_arc["arc_height"])
var collision: Dictionary = resolved_arc["collision"]
return collision.is_empty()
func _resolve_cast_impact_position(origin: Vector3, target: Vector3) -> Vector3:
@ -1410,6 +1515,21 @@ func _get_active_bait_tags() -> Array[StringName]:
return item.bait_tags.duplicate() if item != null and item.is_bait() else []
func _active_lure_has_effect(effect_id: StringName) -> bool:
if (
_local_player == null
or _item_catalog == null
or _local_bag == null
or _local_player.active_lure_id.is_empty()
or not _local_bag.owns_item(_local_player.active_lure_id)
):
return false
var lure: ItemDataType = _item_catalog.get_item_by_id(
_local_player.active_lure_id
)
return lure != null and lure.is_lure() and effect_id in lure.lure_effects
func _build_network_evidence() -> Dictionary:
var rarity_multipliers: Array[float] = []
for rarity: int in range(FishDataType.Rarity.size()):
@ -1421,6 +1541,7 @@ func _build_network_evidence() -> Dictionary:
var item: ItemDataType = _get_active_item()
return {
"bait_id": str(_active_player.active_bait_id),
"lure_id": str(_active_player.active_lure_id),
"rod_id": str(item.item_id) if item != null else "",
"reel_speed": _active_player.reel_speed * (
_fishing_upgrades.get_reel_speed_multiplier()
@ -1441,6 +1562,13 @@ func _on_network_item_use_finished(
accepted: bool,
message: String,
) -> void:
if (
not accepted
and message == PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE
):
status_changed.emit("")
local_speech_requested.emit(message)
return
status_changed.emit(message)
if accepted:
refresh_active_item_status()
@ -1451,15 +1579,22 @@ func _on_network_cast_accepted(
target: Vector3,
) -> void:
if state != FishingState.CASTING:
# A cast response can arrive after water recovery has already cancelled
# its local presentation. Do not leave that late authoritative attempt
# alive with its movement lock still applied.
if _network_fishing != null and _network_fishing.has_local_attempt():
_network_fishing.cancel_local_attempt("")
return
_cast_target = target
_bobber_water_position = target
state = FishingState.WAITING_FOR_BITE
if _active_player != null:
_active_player.set_fishing_visual(true)
_network_primary_input_held = false
_network_input_resend_elapsed = 0.0
_presentation.show_withdrawal_position(_bobber_water_position)
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
status_changed.emit("waiting for a bite...")
status_changed.emit("")
func _on_network_cast_rejected(message: String) -> void:
@ -1478,10 +1613,24 @@ func _on_network_cast_rejected(message: String) -> void:
_cleanup_attempt(visible_message, &"invalid")
func _on_network_bite_pending(_attempt_id: String) -> void:
if state != FishingState.WAITING_FOR_BITE:
return
_stop_reeling_audio()
_withdrawal_input_held = false
_network_primary_input_held = false
_network_input_resend_elapsed = 0.0
_set_bite_confirmation_pending(true)
status_changed.emit("")
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
_presentation.show_bite()
func _on_network_bite_started(_attempt_id: String) -> void:
if state != FishingState.WAITING_FOR_BITE:
return
_stop_reeling_audio()
_set_bite_confirmation_pending(false)
state = FishingState.FIGHTING
if _active_player != null:
_active_player.set_fighting_visual(true)
@ -1512,7 +1661,10 @@ func _resend_network_input_state(delta: float) -> void:
_network_input_resend_elapsed,
NETWORK_INPUT_RESEND_INTERVAL_SECONDS,
)
_network_fishing.submit_local_input(_network_primary_input_held, false)
_network_fishing.submit_local_input(
false if _bite_confirmation_pending else _network_primary_input_held,
_bite_confirmation_pending and _bite_confirmation_requested,
)
func _on_network_fishing_snapshot(snapshot: Dictionary) -> void:
@ -1577,6 +1729,7 @@ func _on_network_catch_received(fish_catch: FishCatchType) -> void:
_pending_catch = fish_catch
if _active_player != null:
_active_player.set_fighting_visual(false)
_active_player.set_fishing_visual(false)
state = FishingState.SHOWING_CATCH
_showcase_ready = false
_showcase_outcome_completed = false
@ -1586,6 +1739,8 @@ func _on_network_catch_received(fish_catch: FishCatchType) -> void:
PackedInt32Array(), -1, false
)
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
if _active_player != null:
_active_player.set_retract_visual()
_presentation.play_outcome(&"catch")

View file

@ -17,6 +17,7 @@ roughness = 0.45
[sub_resource type="StandardMaterial3D" id="LineMaterial"]
shading_mode = 0
cull_mode = 2
albedo_color = Color(0.9, 0.9, 0.82, 1)
[sub_resource type="CylinderMesh" id="CastTargetMesh"]

View file

@ -23,6 +23,7 @@ func resolve_surface(
space_state: PhysicsDirectSpaceState3D,
query_position: Vector3,
reference_y: float,
maximum_solid_rise: float = INF,
) -> FishingSurfaceSampleType:
var sample := FishingSurfaceSampleType.new()
var ray_top: float = maxf(query_position.y, reference_y) + ray_start_height
@ -33,10 +34,16 @@ func resolve_surface(
ray_top,
ray_bottom,
)
var solid_hit: Dictionary = _find_top_solid_surface(
var solid_ray_top: float = minf(
ray_top,
reference_y + maximum_solid_rise,
)
var solid_hit: Dictionary = {}
if solid_ray_top > ray_bottom:
solid_hit = _find_top_solid_surface(
space_state,
query_position,
ray_top,
solid_ray_top,
ray_bottom,
)
@ -101,6 +108,40 @@ func find_first_cast_collision(
return {}
func resolve_cast_arc(
space_state: PhysicsDirectSpaceState3D,
origin: Vector3,
target: Vector3,
preferred_arc_height: float,
) -> Dictionary:
var raised_arc_height: float = maxf(preferred_arc_height, 0.0)
var raised_collision := find_first_cast_collision(
space_state,
origin,
target,
raised_arc_height,
)
if raised_collision.is_empty() or is_zero_approx(raised_arc_height):
return {
"arc_height": raised_arc_height,
"collision": raised_collision,
}
# A canopy or pier roof can intersect the decorative raised arc even when
# the direct path out toward the water is unobstructed. Prefer that low cast
# instead of visibly sending the bobber and line upward into the ceiling.
var direct_collision := find_first_cast_collision(
space_state,
origin,
target,
0.0,
)
return {
"arc_height": 0.0,
"collision": direct_collision,
}
func resolve_withdrawal_surface(
space_state: PhysicsDirectSpaceState3D,
current_position: Vector3,

View file

@ -4,6 +4,7 @@ extends Node3D
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
const LINE_THICKNESS: float = 0.016
signal return_completed
@ -38,6 +39,7 @@ func setup(owning_player: Player) -> void:
_line.mesh = _line_mesh
var line_material := StandardMaterial3D.new()
line_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
line_material.cull_mode = BaseMaterial3D.CULL_DISABLED
line_material.albedo_color = Color(0.9, 0.9, 0.82, 1.0)
_line.material_override = line_material
add_child(_line)
@ -62,6 +64,7 @@ func show_cast(origin: Vector3, target: Vector3) -> void:
_bobber.visible = true
_line.visible = true
_owner.set_active_item_is_rod(true)
_owner.set_release_visual()
_redraw_line()
_cast_tween = create_tween()
_cast_tween.set_trans(Tween.TRANS_SINE)
@ -99,6 +102,7 @@ func show_bite() -> void:
func play_return(showcase_catch: FishCatch = null) -> void:
if _owner != null and is_instance_valid(_owner):
_owner.set_fighting_visual(false)
_owner.set_retract_visual()
if not _active or _bobber == null:
cleanup()
return_completed.emit()
@ -135,6 +139,7 @@ func cleanup() -> void:
_bobber_idle_elapsed = 0.0
if _owner != null and is_instance_valid(_owner):
_owner.set_fighting_visual(false)
_owner.set_fishing_visual(false)
_owner.end_catch_showcase(Callable(), true)
if _bobber != null:
_bobber.visible = false
@ -162,6 +167,8 @@ func _set_cast_sample(
func _on_cast_finished() -> void:
_cast_tween = null
_bobber_idle_elapsed = 0.0
if _owner != null and is_instance_valid(_owner):
_owner.set_fishing_visual(true)
_apply_bobber_idle_motion()
_redraw_line()
@ -194,6 +201,12 @@ func _on_return_finished() -> void:
_bobber.visible = false
_line.visible = false
_line_mesh.clear_surfaces()
if (
_owner != null
and is_instance_valid(_owner)
and not _owner.is_retract_visual_complete()
):
await _owner.retract_visual_finished
if (
_return_showcase_catch != null
and _owner != null
@ -211,7 +224,12 @@ func _on_return_finished() -> void:
func _on_showcase_finished() -> void:
_showcase_tween = null
if _owner != null and is_instance_valid(_owner):
_owner.end_catch_showcase()
_owner.end_catch_showcase(_on_showcase_put_away_finished)
return
_on_showcase_put_away_finished()
func _on_showcase_put_away_finished() -> void:
return_completed.emit()
@ -253,10 +271,50 @@ func _redraw_line() -> void:
if tip == null:
return
_line_mesh.clear_surfaces()
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP)
_line_mesh.surface_add_vertex(to_local(tip.global_position))
var midpoint: Vector3 = tip.global_position.lerp(_target, 0.5)
midpoint.y -= 0.12
_line_mesh.surface_add_vertex(to_local(midpoint))
_line_mesh.surface_add_vertex(to_local(_target))
_draw_line_ribbon(PackedVector3Array([
tip.global_position,
midpoint,
_target,
]))
func _draw_line_ribbon(points: PackedVector3Array) -> void:
var camera: Camera3D = get_viewport().get_camera_3d()
if camera == null:
_line_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP)
for point: Vector3 in points:
_line_mesh.surface_add_vertex(to_local(point))
_line_mesh.surface_end()
return
var half_thickness: float = LINE_THICKNESS * 0.5
_line_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
for point_index: int in range(1, points.size()):
var start: Vector3 = points[point_index - 1]
var end: Vector3 = points[point_index]
var segment: Vector3 = end - start
if segment.length_squared() <= 0.000001:
continue
var to_camera: Vector3 = (
camera.global_position - start.lerp(end, 0.5)
)
var side: Vector3 = segment.normalized().cross(to_camera.normalized())
if side.length_squared() <= 0.000001:
side = segment.normalized().cross(Vector3.UP)
if side.length_squared() <= 0.000001:
side = segment.normalized().cross(Vector3.RIGHT)
side = side.normalized() * half_thickness
var start_left: Vector3 = to_local(start - side)
var start_right: Vector3 = to_local(start + side)
var end_right: Vector3 = to_local(end + side)
var end_left: Vector3 = to_local(end - side)
_line_mesh.surface_add_vertex(start_left)
_line_mesh.surface_add_vertex(start_right)
_line_mesh.surface_add_vertex(end_right)
_line_mesh.surface_add_vertex(start_left)
_line_mesh.surface_add_vertex(end_right)
_line_mesh.surface_add_vertex(end_left)
_line_mesh.surface_end()

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

Before

Width:  |  Height:  |  Size: 995 B

View file

@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
[ext_resource type="Texture2D" path="res://items/icons/placeholder/art_kit.svg" id="2_icon"]
[ext_resource type="Texture2D" path="res://items/icons/art/art_kit.png" id="2_icon"]
[resource]
script = ExtResource("1_item")

View file

@ -0,0 +1,15 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
[resource]
script = ExtResource("1_item")
item_id = &"batteries"
display_name = "batteries"
description = "replacement batteries for the fish finder."
category = 3
stackable = true
max_stack = 99
usable = false
equippable = false
hotbar_allowed = false

View file

@ -7,10 +7,11 @@
script = ExtResource("1")
item_id = &"fish_finder"
display_name = "fish finder"
description = "temporarily shortens bite waits and improves rarity weights."
category = 3
description = "uses one battery to temporarily shorten bite waits and improve rarity weights."
category = 1
icon = ExtResource("2")
stackable = true
max_stack = 99
stackable = false
max_stack = 1
usable = true
equippable = true
hotbar_allowed = true

View file

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=16 format=3]
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=18 format=3]
[ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"]
[ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"]
@ -15,7 +15,9 @@
[ext_resource type="Resource" path="res://items/catalog/whole_anchovy.tres" id="13_anchovy"]
[ext_resource type="Resource" path="res://items/catalog/whole_sardine.tres" id="14_sardine"]
[ext_resource type="Resource" path="res://items/catalog/luminous_roe.tres" id="15_roe"]
[ext_resource type="Resource" path="res://items/catalog/the_standby.tres" id="16_standby"]
[ext_resource type="Resource" path="res://items/catalog/batteries.tres" id="17_batteries"]
[resource]
script = ExtResource("1_script")
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("13_anchovy"), ExtResource("14_sardine"), ExtResource("15_roe")]
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries")]

View file

@ -1,12 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_roe_luminous.png" id="2_icon"]
[resource]
script = ExtResource("1")
item_id = &"luminous_roe"
display_name = "luminous roe"
description = "rare glowing roe with the strongest rare catch odds."
icon = ExtResource("2_icon")
category = 2
bait_tags = Array[StringName]([&"bait_rarity_6"])
stackable = true

View file

@ -1,12 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_shrimp.png" id="2_icon"]
[resource]
script = ExtResource("1")
item_id = &"shrimp"
display_name = "shrimp"
description = "lively bait that noticeably improves rare catch chances."
icon = ExtResource("2_icon")
category = 2
bait_tags = Array[StringName]([&"bait_rarity_2"])
stackable = true

View file

@ -1,12 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_snail.png" id="2_icon"]
[resource]
script = ExtResource("1")
item_id = &"snails"
display_name = "snails"
description = "slow-moving bait with a better chance for rarer catches."
icon = ExtResource("2_icon")
category = 2
bait_tags = Array[StringName]([&"bait_rarity_1"])
stackable = true

View file

@ -1,12 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_squid.png" id="2_icon"]
[resource]
script = ExtResource("1")
item_id = &"squid_chunks"
display_name = "squid chunks"
description = "rich cut bait that strongly favors rarer catches."
icon = ExtResource("2_icon")
category = 2
bait_tags = Array[StringName]([&"bait_rarity_3"])
stackable = true

View file

@ -0,0 +1,16 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
[resource]
script = ExtResource("1_item")
item_id = &"the_standby"
display_name = "The Standby"
description = "holds a bite until you're ready to start the fight."
category = 7
lure_effects = Array[StringName]([&"deferred_fight"])
stackable = false
max_stack = 1
usable = false
equippable = true
hotbar_allowed = false

View file

@ -1,14 +1,16 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_sardine.png" id="2_icon"]
[resource]
script = ExtResource("1")
item_id = &"whole_anchovy"
display_name = "whole anchovy"
description = "premium whole bait with high rare catch potential."
display_name = "sardine"
description = "valuable oily schooling bait tuned for exceptional catches."
icon = ExtResource("2_icon")
category = 2
bait_tags = Array[StringName]([&"bait_rarity_4"])
bait_tags = Array[StringName]([&"bait_rarity_5"])
stackable = true
max_stack = 10
hotbar_allowed = false

View file

@ -1,14 +1,16 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_minnow.png" id="2_icon"]
[resource]
script = ExtResource("1")
item_id = &"whole_sardine"
display_name = "whole sardine"
description = "valuable whole bait tuned for exceptional catches."
display_name = "minnow"
description = "small live bait with high rare catch potential."
icon = ExtResource("2_icon")
category = 2
bait_tags = Array[StringName]([&"bait_rarity_5"])
bait_tags = Array[StringName]([&"bait_rarity_4"])
stackable = true
max_stack = 10
hotbar_allowed = false

BIN
items/icons/art/art_kit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://datu7j7tq2k3e"
path="res://.godot/imported/art_kit.png-b54a66a446203c844f97c37241065423.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://items/icons/art/art_kit.png"
dest_files=["res://.godot/imported/art_kit.png-b54a66a446203c844f97c37241065423.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Some files were not shown because too many files have changed in this diff Show more