Merge alt development work

This commit is contained in:
Alexander Sellite 2026-08-03 17:10:50 -04:00
commit 4177cb19c3
78 changed files with 2280 additions and 72 deletions

48
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,48 @@
# Contributing
NETfishing is currently developed through focused changes on the repository's
active development branch. Coordinate scope with the project owner before
starting substantial work.
## Change discipline
- Keep a change focused; avoid unrelated cleanup.
- Inspect the working tree before editing and preserve existing work.
- Do not change protocol, save, settings, identity, or portable-data versions
as release-label housekeeping.
- Treat the host as authoritative for catches, purchases, sales, jobs, mail,
and other shared state.
- Keep presentation-only systems out of save files and network messages.
- Store runtime assets under repository-owned paths; never reference a
workstation sync or temporary directory.
- Record the source and license of newly imported assets.
- Do not commit `.godot/`, test data, logs, captures, or build outputs.
## Before requesting review
```sh
git diff --check
scripts/run_validations.sh quick
git status --short
```
Choose additional focused tests from [`docs/TESTING.md`](docs/TESTING.md).
Changes involving networking should run the network suite and a real
two-process check. Visual changes still require graphical review; headless
tests are supporting evidence, not a replacement.
## Commits
- Stage audited paths explicitly.
- Use a concise imperative subject that describes the outcome.
- Do not mix generated build artifacts with source changes.
- Do not rewrite published release tags.
- Describe behavior, compatibility impact, validation, and asset provenance in
the review or release record.
## Style
Follow the conventions already present in the surrounding GDScript, scene, and
resource files. Prefer typed values, named constants, shared resources, and
small domain-specific services. Avoid duplicating protocol definitions or
using display strings as persistent identifiers.

50
CREDITS.md Normal file
View file

@ -0,0 +1,50 @@
# 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/
- “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).

82
README.md Normal file
View file

@ -0,0 +1,82 @@
# NETfishing
NETfishing is a multiplayer-first fishing game built with Godot 4.7. The
current pre-alpha includes host-authoritative fishing and economy flows,
freshwater and saltwater habitats, persistent player data, social features,
customization, jobs, and a controller-aware interface.
The project is under active development. Interfaces, balance, content, and
save migrations may continue to change before a stable release.
## Requirements
- Godot 4.7.x with the GL Compatibility renderer
- Linux, Windows, or an Android development environment supported by Godot
- Bash for the repository helper scripts
Open `project.godot` in Godot, or start the editor from the repository root:
```sh
godot --editor --path .
```
Run the game directly with:
```sh
godot --path .
```
## Validation
The validation scripts isolate Godot's settings and application-data roots.
Run the fast, deterministic suite before submitting a change:
```sh
scripts/run_validations.sh quick
```
Additional suites and individual commands are documented in
[`docs/TESTING.md`](docs/TESTING.md).
## Building
Export presets are maintained for Linux, Windows, and Android. Repository
scripts expect the matching Godot export templates and platform toolchains to
already be installed:
```sh
scripts/build_playtest.sh
scripts/build_android_debug.sh
```
Build outputs belong under the ignored `builds/` directory. Release builds
must follow the project's separate tagged-release checklist; these convenience
scripts do not create or publish tags.
## Project map
- `main/`: composition root and application orchestration
- `world/`: map, environment, water, and world presentation
- `player/`, `fishing/`, `fish/`: player and fishing domain
- `network/`: transport, identity, and host-authoritative services
- `inventory/`, `items/`, `economy/`, `progression/`, `jobs/`: game state
- `save/`, `settings/`: versioned persistence
- `ui/`: gameplay and menu presentation
- `tests/`: focused Godot validation scripts
- `scripts/`: build, bake, and validation entry points
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and
[`world/README-WORLD-AUTHORING.md`](world/README-WORLD-AUTHORING.md) for more
detail.
## Authorship and assets
NETfishing is human-directed and uses a mixture of human-authored 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).
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.

BIN
audio/ambience/waves.wav Normal file

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dck3tf8qkadea"
path="res://.godot/imported/waves.wav-90ffb886ce064ba838eebb25ec72a17e.sample"
[deps]
source_file="res://audio/ambience/waves.wav"
dest_files=["res://.godot/imported/waves.wav-90ffb886ce064ba838eebb25ec72a17e.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=1
edit/loop_begin=0
edit/loop_end=1044956
compress/mode=0

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://sxydt4wnmrj3"
path="res://.godot/imported/bobber.wav-b435bb251a112445f1ddebcacc74804a.sample"
[deps]
source_file="res://audio/sfx/fishing/bobber.wav"
dest_files=["res://.godot/imported/bobber.wav-b435bb251a112445f1ddebcacc74804a.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://df6yg85cldetw"
path="res://.godot/imported/fighting.wav-1427843e18acf5134cc66d4a813033b6.sample"
[deps]
source_file="res://audio/sfx/fishing/fighting.wav"
dest_files=["res://.godot/imported/fighting.wav-1427843e18acf5134cc66d4a813033b6.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=1
edit/loop_begin=0
edit/loop_end=124487
compress/mode=0

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://v4yjxtslbrd7"
path="res://.godot/imported/reeling.wav-97a9415ae2d9e7ca4c976bb15b10e6fd.sample"
[deps]
source_file="res://audio/sfx/fishing/reeling.wav"
dest_files=["res://.godot/imported/reeling.wav-97a9415ae2d9e7ca4c976bb15b10e6fd.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=1
edit/loop_begin=0
edit/loop_end=114628
compress/mode=0

View file

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

72
docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,72 @@
# 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).

109
docs/ASSET-PROVENANCE.md Normal file
View file

@ -0,0 +1,109 @@
# 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.

41
docs/BALANCE.md Normal file
View file

@ -0,0 +1,41 @@
# 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.

63
docs/TESTING.md Normal file
View file

@ -0,0 +1,63 @@
# 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

@ -0,0 +1,27 @@
# 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

@ -0,0 +1,27 @@
# 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

@ -1,4 +1,4 @@
[gd_resource type="Resource" load_steps=10 format=3] [gd_resource type="Resource" load_steps=21 format=3]
[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"] [ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
[ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="2_bluegill"] [ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="2_bluegill"]
@ -9,7 +9,18 @@
[ext_resource type="Resource" path="res://fish/species/catfish_channel/catfish_channel.tres" id="7_catfish_channel"] [ext_resource type="Resource" path="res://fish/species/catfish_channel/catfish_channel.tres" id="7_catfish_channel"]
[ext_resource type="Resource" path="res://fish/species/catfish_flathead/catfish_flathead.tres" id="8_catfish_flathead"] [ext_resource type="Resource" path="res://fish/species/catfish_flathead/catfish_flathead.tres" id="8_catfish_flathead"]
[ext_resource type="Resource" path="res://fish/species/catfish_white/catfish_white.tres" id="9_catfish_white"] [ext_resource type="Resource" path="res://fish/species/catfish_white/catfish_white.tres" id="9_catfish_white"]
[ext_resource type="Resource" path="res://fish/species/tuna_albacore/tuna_albacore.tres" id="10_tuna_albacore"]
[ext_resource type="Resource" path="res://fish/species/tuna_bigeye/tuna_bigeye.tres" id="11_tuna_bigeye"]
[ext_resource type="Resource" path="res://fish/species/tuna_bluefin/tuna_bluefin.tres" id="12_tuna_bluefin"]
[ext_resource type="Resource" path="res://fish/species/tuna_skipjack/tuna_skipjack.tres" id="13_tuna_skipjack"]
[ext_resource type="Resource" path="res://fish/species/tuna_yellowfin/tuna_yellowfin.tres" id="14_tuna_yellowfin"]
[ext_resource type="Resource" path="res://fish/species/goby_round/goby_round.tres" id="15_goby_round"]
[ext_resource type="Resource" path="res://fish/species/salmon_atlantic/salmon_atlantic.tres" id="16_salmon_atlantic"]
[ext_resource type="Resource" path="res://fish/species/salmon_chum/salmon_chum.tres" id="17_salmon_chum"]
[ext_resource type="Resource" path="res://fish/species/salmon_coho/salmon_coho.tres" id="18_salmon_coho"]
[ext_resource type="Resource" path="res://fish/species/salmon_pink/salmon_pink.tres" id="19_salmon_pink"]
[ext_resource type="Resource" path="res://fish/species/salmon_sockeye/salmon_sockeye.tres" id="20_salmon_sockeye"]
[resource] [resource]
script = ExtResource("1_pool") script = ExtResource("1_pool")
candidates = [ExtResource("2_bluegill"), ExtResource("3_bass"), ExtResource("4_carp"), ExtResource("5_sunfish"), ExtResource("6_catfish_blue"), ExtResource("7_catfish_channel"), ExtResource("8_catfish_flathead"), ExtResource("9_catfish_white")] candidates = [ExtResource("2_bluegill"), ExtResource("3_bass"), ExtResource("4_carp"), ExtResource("5_sunfish"), ExtResource("6_catfish_blue"), ExtResource("7_catfish_channel"), ExtResource("8_catfish_flathead"), ExtResource("9_catfish_white"), ExtResource("10_tuna_albacore"), ExtResource("11_tuna_bigeye"), ExtResource("12_tuna_bluefin"), ExtResource("13_tuna_skipjack"), ExtResource("14_tuna_yellowfin"), ExtResource("15_goby_round"), ExtResource("16_salmon_atlantic"), ExtResource("17_salmon_chum"), ExtResource("18_salmon_coho"), ExtResource("19_salmon_pink"), ExtResource("20_salmon_sockeye")]

View file

@ -1,9 +1,19 @@
[gd_resource type="Resource" load_steps=4 format=3] [gd_resource type="Resource" load_steps=14 format=3]
[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"] [ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
[ext_resource type="Resource" path="res://fish/species/bass/bass.tres" id="2_bass"] [ext_resource type="Resource" path="res://fish/species/bass/bass.tres" id="2_bass"]
[ext_resource type="Resource" path="res://fish/species/sunfish/sunfish.tres" id="3_sunfish"] [ext_resource type="Resource" path="res://fish/species/sunfish/sunfish.tres" id="3_sunfish"]
[ext_resource type="Resource" path="res://fish/species/tuna_albacore/tuna_albacore.tres" id="4_tuna_albacore"]
[ext_resource type="Resource" path="res://fish/species/tuna_bigeye/tuna_bigeye.tres" id="5_tuna_bigeye"]
[ext_resource type="Resource" path="res://fish/species/tuna_bluefin/tuna_bluefin.tres" id="6_tuna_bluefin"]
[ext_resource type="Resource" path="res://fish/species/tuna_skipjack/tuna_skipjack.tres" id="7_tuna_skipjack"]
[ext_resource type="Resource" path="res://fish/species/tuna_yellowfin/tuna_yellowfin.tres" id="8_tuna_yellowfin"]
[ext_resource type="Resource" path="res://fish/species/salmon_atlantic/salmon_atlantic.tres" id="9_salmon_atlantic"]
[ext_resource type="Resource" path="res://fish/species/salmon_chum/salmon_chum.tres" id="10_salmon_chum"]
[ext_resource type="Resource" path="res://fish/species/salmon_coho/salmon_coho.tres" id="11_salmon_coho"]
[ext_resource type="Resource" path="res://fish/species/salmon_pink/salmon_pink.tres" id="12_salmon_pink"]
[ext_resource type="Resource" path="res://fish/species/salmon_sockeye/salmon_sockeye.tres" id="13_salmon_sockeye"]
[resource] [resource]
script = ExtResource("1_pool") script = ExtResource("1_pool")
candidates = [ExtResource("2_bass"), ExtResource("3_sunfish")] candidates = [ExtResource("2_bass"), ExtResource("3_sunfish"), ExtResource("4_tuna_albacore"), ExtResource("5_tuna_bigeye"), ExtResource("6_tuna_bluefin"), ExtResource("7_tuna_skipjack"), ExtResource("8_tuna_yellowfin"), ExtResource("9_salmon_atlantic"), ExtResource("10_salmon_chum"), ExtResource("11_salmon_coho"), ExtResource("12_salmon_pink"), ExtResource("13_salmon_sockeye")]

View file

@ -1,4 +1,4 @@
[gd_resource type="Resource" load_steps=8 format=3] [gd_resource type="Resource" load_steps=9 format=3]
[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"] [ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
[ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="2_bluegill"] [ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="2_bluegill"]
@ -7,7 +7,8 @@
[ext_resource type="Resource" path="res://fish/species/catfish_channel/catfish_channel.tres" id="7_catfish_channel"] [ext_resource type="Resource" path="res://fish/species/catfish_channel/catfish_channel.tres" id="7_catfish_channel"]
[ext_resource type="Resource" path="res://fish/species/catfish_flathead/catfish_flathead.tres" id="8_catfish_flathead"] [ext_resource type="Resource" path="res://fish/species/catfish_flathead/catfish_flathead.tres" id="8_catfish_flathead"]
[ext_resource type="Resource" path="res://fish/species/catfish_white/catfish_white.tres" id="9_catfish_white"] [ext_resource type="Resource" path="res://fish/species/catfish_white/catfish_white.tres" id="9_catfish_white"]
[ext_resource type="Resource" path="res://fish/species/goby_round/goby_round.tres" id="10_goby_round"]
[resource] [resource]
script = ExtResource("1_pool") script = ExtResource("1_pool")
candidates = [ExtResource("2_bluegill"), ExtResource("4_carp"), ExtResource("6_catfish_blue"), ExtResource("7_catfish_channel"), ExtResource("8_catfish_flathead"), ExtResource("9_catfish_white")] candidates = [ExtResource("2_bluegill"), ExtResource("4_carp"), ExtResource("6_catfish_blue"), ExtResource("7_catfish_channel"), ExtResource("8_catfish_flathead"), ExtResource("9_catfish_white"), ExtResource("10_goby_round")]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/goby_round/fish_goby_round.png" id="4_texture"]
[sub_resource type="Resource" id="RoundGobyAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"worm", &"minnow"])
preferred_bait_weight_multiplier = 1.35
[resource]
script = ExtResource("1_fish_data")
id = &"goby_round"
display_name = "round goby"
allowed_water_types = 1
rarity = 0
base_catch_weight = 8.0
catch_profile = ExtResource("2_profile")
availability = SubResource("RoundGobyAvailability")
weight_min_lb = 0.1
weight_max_lb = 0.6
display_scale_min = 0.55
display_scale_max = 0.9
display_scale_curve = 1.0
sell_value_min = 2
sell_value_max = 3
sell_value_curve = 1.0
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/salmon_atlantic/fish_salmon_atlantic.png" id="4_texture"]
[sub_resource type="Resource" id="AtlanticSalmonAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"salmon_atlantic"
display_name = "atlantic salmon"
allowed_water_types = 2
rarity = 3
base_catch_weight = 0.65
catch_profile = ExtResource("2_profile")
availability = SubResource("AtlanticSalmonAvailability")
weight_min_lb = 8.0
weight_max_lb = 40.0
display_scale_min = 1.05
display_scale_max = 1.9
display_scale_curve = 0.85
sell_value_min = 20
sell_value_max = 42
sell_value_curve = 0.85
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/salmon_chum/fish_salmon_chum.png" id="4_texture"]
[sub_resource type="Resource" id="ChumSalmonAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"salmon_chum"
display_name = "chum salmon"
allowed_water_types = 2
rarity = 1
base_catch_weight = 2.5
catch_profile = ExtResource("2_profile")
availability = SubResource("ChumSalmonAvailability")
weight_min_lb = 6.0
weight_max_lb = 25.0
display_scale_min = 1.0
display_scale_max = 1.75
display_scale_curve = 0.9
sell_value_min = 10
sell_value_max = 22
sell_value_curve = 0.9
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/salmon_coho/fish_salmon_coho.png" id="4_texture"]
[sub_resource type="Resource" id="CohoSalmonAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"salmon_coho"
display_name = "coho salmon"
allowed_water_types = 2
rarity = 2
base_catch_weight = 1.4
catch_profile = ExtResource("2_profile")
availability = SubResource("CohoSalmonAvailability")
weight_min_lb = 5.0
weight_max_lb = 30.0
display_scale_min = 1.0
display_scale_max = 1.8
display_scale_curve = 0.88
sell_value_min = 14
sell_value_max = 28
sell_value_curve = 0.88
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/salmon_pink/fish_salmon_pink.png" id="4_texture"]
[sub_resource type="Resource" id="PinkSalmonAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"salmon_pink"
display_name = "pink salmon"
allowed_water_types = 2
rarity = 1
base_catch_weight = 3.0
catch_profile = ExtResource("2_profile")
availability = SubResource("PinkSalmonAvailability")
weight_min_lb = 3.0
weight_max_lb = 12.0
display_scale_min = 0.9
display_scale_max = 1.55
display_scale_curve = 0.92
sell_value_min = 8
sell_value_max = 15
sell_value_curve = 0.92
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/salmon_sockeye/fish_salmon_sockeye.png" id="4_texture"]
[sub_resource type="Resource" id="SockeyeSalmonAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"salmon_sockeye"
display_name = "sockeye salmon"
allowed_water_types = 2
rarity = 2
base_catch_weight = 1.6
catch_profile = ExtResource("2_profile")
availability = SubResource("SockeyeSalmonAvailability")
weight_min_lb = 4.0
weight_max_lb = 15.0
display_scale_min = 0.95
display_scale_max = 1.6
display_scale_curve = 0.9
sell_value_min = 12
sell_value_max = 24
sell_value_curve = 0.9
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/tuna_albacore/fish_tuna_albacore.png" id="4_texture"]
[sub_resource type="Resource" id="AlbacoreAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"tuna_albacore"
display_name = "albacore tuna"
allowed_water_types = 2
rarity = 2
base_catch_weight = 1.5
catch_profile = ExtResource("2_profile")
availability = SubResource("AlbacoreAvailability")
weight_min_lb = 10.0
weight_max_lb = 45.0
display_scale_min = 1.1
display_scale_max = 1.9
display_scale_curve = 0.85
sell_value_min = 12
sell_value_max = 24
sell_value_curve = 0.85
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/tuna_bigeye/fish_tuna_bigeye.png" id="4_texture"]
[sub_resource type="Resource" id="BigeyeAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"tuna_bigeye"
display_name = "bigeye tuna"
allowed_water_types = 2
rarity = 3
base_catch_weight = 0.55
catch_profile = ExtResource("2_profile")
availability = SubResource("BigeyeAvailability")
weight_min_lb = 20.0
weight_max_lb = 120.0
display_scale_min = 1.2
display_scale_max = 2.2
display_scale_curve = 0.8
sell_value_min = 22
sell_value_max = 48
sell_value_curve = 0.8
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/tuna_bluefin/fish_tuna_bluefin.png" id="4_texture"]
[sub_resource type="Resource" id="BluefinAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"tuna_bluefin"
display_name = "bluefin tuna"
allowed_water_types = 2
rarity = 4
base_catch_weight = 0.18
catch_profile = ExtResource("2_profile")
availability = SubResource("BluefinAvailability")
weight_min_lb = 30.0
weight_max_lb = 250.0
display_scale_min = 1.3
display_scale_max = 2.5
display_scale_curve = 0.78
sell_value_min = 35
sell_value_max = 90
sell_value_curve = 0.78
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/tuna_skipjack/fish_tuna_skipjack.png" id="4_texture"]
[sub_resource type="Resource" id="SkipjackAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"tuna_skipjack"
display_name = "skipjack tuna"
allowed_water_types = 2
rarity = 1
base_catch_weight = 3.0
catch_profile = ExtResource("2_profile")
availability = SubResource("SkipjackAvailability")
weight_min_lb = 5.0
weight_max_lb = 25.0
display_scale_min = 1.0
display_scale_max = 1.7
display_scale_curve = 0.9
sell_value_min = 8
sell_value_max = 16
sell_value_curve = 0.9
display_texture = ExtResource("4_texture")

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

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

View file

@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
[ext_resource type="Texture2D" path="res://fish/species/tuna_yellowfin/fish_tuna_yellowfin.png" id="4_texture"]
[sub_resource type="Resource" id="YellowfinAvailability"]
script = ExtResource("3_availability_script")
preferred_bait_tags = Array[StringName]([&"minnow"])
preferred_bait_weight_multiplier = 1.4
[resource]
script = ExtResource("1_fish_data")
id = &"tuna_yellowfin"
display_name = "yellowfin tuna"
allowed_water_types = 2
rarity = 2
base_catch_weight = 1.0
catch_profile = ExtResource("2_profile")
availability = SubResource("YellowfinAvailability")
weight_min_lb = 15.0
weight_max_lb = 80.0
display_scale_min = 1.15
display_scale_max = 2.1
display_scale_curve = 0.82
sell_value_min = 16
sell_value_max = 36
sell_value_curve = 0.82
display_texture = ExtResource("4_texture")

View file

@ -122,6 +122,9 @@ const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
@onready var _catch_controller: CatchControllerType = %CatchController @onready var _catch_controller: CatchControllerType = %CatchController
@onready var _presentation: FishingPresentationType = %FishingPresentation @onready var _presentation: FishingPresentationType = %FishingPresentation
@onready var _bobber_audio: AudioStreamPlayer = %BobberAudio
@onready var _fight_audio: AudioStreamPlayer = %FightAudio
@onready var _reeling_audio: AudioStreamPlayer = %ReelingAudio
var state: FishingState = FishingState.READY var state: FishingState = FishingState.READY
var _external_input_blocked: bool = false var _external_input_blocked: bool = false
@ -177,6 +180,12 @@ var _pending_cleanup_message: String = ""
func _ready() -> void: func _ready() -> void:
_bite_rng.randomize() _bite_rng.randomize()
_configure_surface_resolver() _configure_surface_resolver()
var fight_stream := _fight_audio.stream as AudioStreamWAV
if fight_stream != null:
_configure_audio_loop(fight_stream)
var reeling_stream := _reeling_audio.stream as AudioStreamWAV
if reeling_stream != null:
_configure_audio_loop(reeling_stream)
_catch_controller.encounter_updated.connect(_on_catch_encounter_updated) _catch_controller.encounter_updated.connect(_on_catch_encounter_updated)
_catch_controller.caught.connect(_on_catch_completed) _catch_controller.caught.connect(_on_catch_completed)
_catch_controller.escaped.connect(_on_catch_escaped) _catch_controller.escaped.connect(_on_catch_escaped)
@ -223,6 +232,15 @@ func setup(
_network_fishing = network_fishing _network_fishing = network_fishing
_world_time = world_time _world_time = world_time
_world_weather = world_weather _world_weather = world_weather
if (
_network_session != null
and not _network_session.state_changed.is_connected(
_on_network_session_state_changed
)
):
_network_session.state_changed.connect(
_on_network_session_state_changed
)
if _network_fishing != null: if _network_fishing != null:
_network_fishing.local_cast_accepted.connect( _network_fishing.local_cast_accepted.connect(
_on_network_cast_accepted _on_network_cast_accepted
@ -348,6 +366,7 @@ func set_local_menu_input_suppressed(
_local_menu_input_owners.erase(owner) _local_menu_input_owners.erase(owner)
if suppressed and state == FishingState.WAITING_FOR_BITE: if suppressed and state == FishingState.WAITING_FOR_BITE:
_withdrawal_input_held = false _withdrawal_input_held = false
_stop_reeling_audio()
_presentation.set_line_mode( _presentation.set_line_mode(
FishingPresentationType.LineMode.SLACK FishingPresentationType.LineMode.SLACK
) )
@ -357,6 +376,7 @@ func set_gameplay_input_enabled(enabled: bool) -> void:
_gameplay_input_enabled = enabled _gameplay_input_enabled = enabled
if not enabled: if not enabled:
_local_menu_input_owners.clear() _local_menu_input_owners.clear()
_stop_reeling_audio()
func configure_accessibility_auto_click( func configure_accessibility_auto_click(
@ -400,6 +420,8 @@ func _secure_showcase_catch_for_recovery() -> void:
func _exit_tree() -> void: func _exit_tree() -> void:
_stop_fight_audio()
_stop_reeling_audio()
_showcase_restore_generation += 1 _showcase_restore_generation += 1
if ( if (
state == FishingState.SHOWING_CATCH state == FishingState.SHOWING_CATCH
@ -531,6 +553,7 @@ func _unhandled_input(event: InputEvent) -> void:
return return
FishingState.WAITING_FOR_BITE: FishingState.WAITING_FOR_BITE:
_withdrawal_input_held = true _withdrawal_input_held = true
_start_reeling_audio()
if _network_fishing != null: if _network_fishing != null:
_network_primary_input_held = true _network_primary_input_held = true
_network_input_resend_elapsed = 0.0 _network_input_resend_elapsed = 0.0
@ -546,6 +569,7 @@ func _unhandled_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
elif state == FishingState.WAITING_FOR_BITE: elif state == FishingState.WAITING_FOR_BITE:
_withdrawal_input_held = false _withdrawal_input_held = false
_stop_reeling_audio()
if _network_fishing != null: if _network_fishing != null:
_network_primary_input_held = false _network_primary_input_held = false
_network_input_resend_elapsed = 0.0 _network_input_resend_elapsed = 0.0
@ -731,6 +755,7 @@ func _on_cast_completed() -> void:
if not _is_cast_target_valid(_cast_target): if not _is_cast_target_valid(_cast_target):
_cleanup_attempt("", &"invalid") _cleanup_attempt("", &"invalid")
return return
_play_bobber_audio()
if _network_fishing != null: if _network_fishing != null:
_network_fishing.request_local_cast( _network_fishing.request_local_cast(
_cast_origin_position, _cast_origin_position,
@ -816,6 +841,7 @@ func _update_waiting_for_bite(delta: float) -> void:
and not Input.is_action_pressed("fish_primary") and not Input.is_action_pressed("fish_primary")
): ):
_withdrawal_input_held = false _withdrawal_input_held = false
_stop_reeling_audio()
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK) _presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
if not _withdrawal_input_held: if not _withdrawal_input_held:
@ -908,6 +934,7 @@ func _activate_bite() -> void:
if state != FishingState.WAITING_FOR_BITE: if state != FishingState.WAITING_FOR_BITE:
return return
_stop_reeling_audio()
if ( if (
_active_player == null _active_player == null
or _selected_fish == null or _selected_fish == null
@ -923,6 +950,7 @@ func _activate_bite() -> void:
if _pending_catch == null or not _pending_catch.is_valid(): if _pending_catch == null or not _pending_catch.is_valid():
_cancel_attempt() _cancel_attempt()
return return
_start_fight_audio()
bite_activated.emit() bite_activated.emit()
status_changed.emit("fish on!") status_changed.emit("fish on!")
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
@ -1001,6 +1029,7 @@ func _on_catch_completed() -> void:
if _pending_catch == null or not _pending_catch.is_valid(): if _pending_catch == null or not _pending_catch.is_valid():
_cancel_attempt() _cancel_attempt()
return return
_stop_fight_audio()
state = FishingState.SHOWING_CATCH state = FishingState.SHOWING_CATCH
_showcase_ready = false _showcase_ready = false
_showcase_outcome_completed = false _showcase_outcome_completed = false
@ -1014,6 +1043,7 @@ func _on_catch_escaped() -> void:
if state != FishingState.FIGHTING: if state != FishingState.FIGHTING:
return return
_stop_fight_audio()
var fish_name: String = "the fish" var fish_name: String = "the fish"
if _selected_fish != null and not _selected_fish.display_name.is_empty(): if _selected_fish != null and not _selected_fish.display_name.is_empty():
fish_name = "the %s" % _selected_fish.display_name fish_name = "the %s" % _selected_fish.display_name
@ -1120,6 +1150,8 @@ func _cleanup_attempt(
cooldown_message: String = "", cooldown_message: String = "",
visual_outcome: StringName = &"", visual_outcome: StringName = &"",
) -> void: ) -> void:
_stop_fight_audio()
_stop_reeling_audio()
if not visual_outcome.is_empty(): if not visual_outcome.is_empty():
if state == FishingState.RETURNING: if state == FishingState.RETURNING:
return return
@ -1422,12 +1454,14 @@ func _on_network_cast_rejected(message: String) -> void:
func _on_network_bite_started(_attempt_id: String) -> void: func _on_network_bite_started(_attempt_id: String) -> void:
if state != FishingState.WAITING_FOR_BITE: if state != FishingState.WAITING_FOR_BITE:
return return
_stop_reeling_audio()
state = FishingState.FIGHTING state = FishingState.FIGHTING
_withdrawal_input_held = false _withdrawal_input_held = false
_network_primary_input_held = Input.is_action_pressed("fish_primary") _network_primary_input_held = Input.is_action_pressed("fish_primary")
_network_input_resend_elapsed = 0.0 _network_input_resend_elapsed = 0.0
_network_auto_click_accumulator = 0.0 _network_auto_click_accumulator = 0.0
_network_active_barrier_index = -1 _network_active_barrier_index = -1
_start_fight_audio()
bite_activated.emit() bite_activated.emit()
status_changed.emit("fish on!") status_changed.emit("fish on!")
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
@ -1510,6 +1544,7 @@ func _update_network_auto_click(delta: float) -> void:
func _on_network_catch_received(fish_catch: FishCatchType) -> void: func _on_network_catch_received(fish_catch: FishCatchType) -> void:
if state != FishingState.FIGHTING or fish_catch == null: if state != FishingState.FIGHTING or fish_catch == null:
return return
_stop_fight_audio()
_pending_catch = fish_catch _pending_catch = fish_catch
state = FishingState.SHOWING_CATCH state = FishingState.SHOWING_CATCH
_showcase_ready = false _showcase_ready = false
@ -1541,6 +1576,57 @@ func _on_network_attempt_ended(
_cleanup_attempt(message, visual_outcome) _cleanup_attempt(message, visual_outcome)
func _on_network_session_state_changed(state_value: NetworkSession.State) -> void:
if state_value in [
NetworkSession.State.INACTIVE,
NetworkSession.State.DISCONNECTING,
NetworkSession.State.CONNECTION_FAILED,
NetworkSession.State.SERVER_LOST,
]:
_stop_fight_audio()
_stop_reeling_audio()
func _start_fight_audio() -> void:
if _fight_audio == null or _fight_audio.stream == null:
return
if not _fight_audio.playing:
_fight_audio.play()
func _play_bobber_audio() -> void:
if _bobber_audio == null or _bobber_audio.stream == null:
return
_bobber_audio.play()
func _stop_fight_audio() -> void:
if _fight_audio != null and _fight_audio.playing:
_fight_audio.stop()
func _configure_audio_loop(stream: AudioStreamWAV) -> void:
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
if stream.loop_end <= stream.loop_begin:
stream.loop_begin = 0
stream.loop_end = maxi(
1,
roundi(stream.get_length() * float(stream.mix_rate)),
)
func _start_reeling_audio() -> void:
if _reeling_audio == null or _reeling_audio.stream == null:
return
if not _reeling_audio.playing:
_reeling_audio.play()
func _stop_reeling_audio() -> void:
if _reeling_audio != null and _reeling_audio.playing:
_reeling_audio.stop()
func resolve_safe_withdrawal_surface( func resolve_safe_withdrawal_surface(
from: Vector3, from: Vector3,
to: Vector3, to: Vector3,

View file

@ -1,6 +1,9 @@
[gd_scene load_steps=12 format=3] [gd_scene load_steps=15 format=3]
[ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"] [ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"]
[ext_resource type="AudioStream" path="res://audio/sfx/fishing/fighting.wav" id="2_fighting_audio"]
[ext_resource type="AudioStream" path="res://audio/sfx/fishing/reeling.wav" id="2_reeling_audio"]
[ext_resource type="AudioStream" path="res://audio/sfx/fishing/bobber.wav" id="2_bobber_audio"]
[ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"] [ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"]
[ext_resource type="Script" path="res://fishing/catch_controller.gd" id="4_catch"] [ext_resource type="Script" path="res://fishing/catch_controller.gd" id="4_catch"]
@ -41,6 +44,24 @@ size = Vector3(0.62, 0.04, 0.09)
[node name="FishingSpot" type="Node3D"] [node name="FishingSpot" type="Node3D"]
script = ExtResource("1_spot") script = ExtResource("1_spot")
[node name="BobberAudio" type="AudioStreamPlayer" parent="."]
unique_name_in_owner = true
stream = ExtResource("2_bobber_audio")
volume_db = -14.0
bus = &"SFX"
[node name="FightAudio" type="AudioStreamPlayer" parent="."]
unique_name_in_owner = true
stream = ExtResource("2_fighting_audio")
volume_db = -10.0
bus = &"SFX"
[node name="ReelingAudio" type="AudioStreamPlayer" parent="."]
unique_name_in_owner = true
stream = ExtResource("2_reeling_audio")
volume_db = -10.0
bus = &"SFX"
[node name="CatchController" type="Node" parent="."] [node name="CatchController" type="Node" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("4_catch") script = ExtResource("4_catch")

View file

@ -201,9 +201,15 @@ var _application_initialized := false
var _pending_existing_root_path := "" var _pending_existing_root_path := ""
var _local_recovery_attempt_id: String = "" var _local_recovery_attempt_id: String = ""
@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience
func _ready() -> void: func _ready() -> void:
DisplayServer.window_set_title("NETfishing") DisplayServer.window_set_title("NETfishing")
_shoreline_ambience.configure(
_player,
_test_world.get_saltwater_shoreline_mesh(),
)
var menu_pattern_material := ( var menu_pattern_material := (
_player_menu_backdrop.material as ShaderMaterial _player_menu_backdrop.material as ShaderMaterial
) )
@ -1026,6 +1032,7 @@ func _reset_pixelation() -> void:
func _set_gameplay_active(active: bool) -> void: func _set_gameplay_active(active: bool) -> void:
_gameplay_started = active _gameplay_started = active
_shoreline_ambience.set_active(active)
_title_background.visible = not active _title_background.visible = not active
_world_pixelation.set_gameplay_active(active) _world_pixelation.set_gameplay_active(active)
_ui_pixelation.set_gameplay_active(active) _ui_pixelation.set_gameplay_active(active)

View file

@ -53,6 +53,8 @@
[ext_resource type="Script" path="res://network/network_world_weather_service.gd" id="51_network_world_weather"] [ext_resource type="Script" path="res://network/network_world_weather_service.gd" id="51_network_world_weather"]
[ext_resource type="Script" path="res://jobs/player_job_service.gd" id="52_player_jobs"] [ext_resource type="Script" path="res://jobs/player_job_service.gd" id="52_player_jobs"]
[ext_resource type="Script" path="res://network/network_job_service.gd" id="53_network_jobs"] [ext_resource type="Script" path="res://network/network_job_service.gd" id="53_network_jobs"]
[ext_resource type="Script" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
[ext_resource type="AudioStream" path="res://audio/ambience/waves.wav" id="55_waves"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
shader = ExtResource("40_title_water") shader = ExtResource("40_title_water")
@ -277,6 +279,15 @@ stream = ExtResource("13_title_music")
volume_db = -80.0 volume_db = -80.0
bus = &"Music" bus = &"Music"
[node name="ShorelineAmbience" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("54_shoreline_ambience")
[node name="WavesAudio" type="AudioStreamPlayer" parent="ShorelineAmbience"]
unique_name_in_owner = true
stream = ExtResource("55_waves")
bus = &"SFX"
[node name="WorldPixelationPostprocess" parent="." unique_id=344378000 instance=ExtResource("16_world_pixelation")] [node name="WorldPixelationPostprocess" parent="." unique_id=344378000 instance=ExtResource("16_world_pixelation")]
unique_name_in_owner = true unique_name_in_owner = true

186
scripts/run_validations.sh Executable file
View file

@ -0,0 +1,186 @@
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
readonly GODOT_BIN="${GODOT_BIN:-godot}"
readonly TEST_TIMEOUT_SECONDS="${TEST_TIMEOUT_SECONDS:-120}"
readonly RUN_ROOT="$(mktemp -d -t netfishing-validations.XXXXXX)"
readonly -a QUICK_TESTS=(
"tests/android_readiness_validation.gd"
"tests/fish_catalog_content_validation.gd"
"tests/fish_quality_validation.gd"
"tests/fishing_audio_validation.gd"
"tests/fishing_surface_validation.gd"
"tests/logbook_validation.gd"
"tests/player_experience_validation.gd"
"tests/shoreline_ambience_validation.gd"
"tests/surface_drawing_validation.gd"
"tests/terrain_projection_validation.gd"
"tests/world_time_validation.gd"
"tests/world_weather_validation.gd"
)
readonly -a RUNTIME_TESTS=(
"tests/inventory_notepad_art_validation.gd"
"tests/logbook_runtime_validation.gd"
"tests/player_experience_ui_validation.gd"
"tests/ui_scaling_runtime_validation.gd"
)
readonly -a HOST_TESTS=(
"tests/art_tools_validation.gd"
"tests/economy_regression_validation.gd"
"tests/fish_hotbar_showcase_validation.gd"
"tests/fishing_authority_validation.gd"
"tests/job_system_validation.gd"
"tests/surface_drawing_runtime_validation.gd"
)
readonly -a NETWORK_TESTS=(
"tests/economy_regression_validation.gd"
"tests/fish_showcase_multiplayer_validation.gd"
"tests/fishing_multiplayer_validation.gd"
"tests/job_multiplayer_validation.gd"
"tests/surface_drawing_multiplayer_validation.gd"
"tests/world_time_multiplayer_validation.gd"
)
cleanup() {
rm -rf -- "${RUN_ROOT}"
}
trap cleanup EXIT INT TERM
usage() {
printf 'Usage: %s {quick|full|host|network|all|--list}\n' "$0"
}
prepare_root() {
local name="$1"
local root="${RUN_ROOT}/${name}"
mkdir -p -- "${root}/data" "${root}/config" "${root}/cache"
printf '%s\n' "${root}"
}
run_test() {
local script="$1"
local name="${script#tests/}"
name="${name%.gd}"
local root
root="$(prepare_root "${name}")"
printf '\n==> %s\n' "${script}"
XDG_DATA_HOME="${root}/data" \
XDG_CONFIG_HOME="${root}/config" \
XDG_CACHE_HOME="${root}/cache" \
timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
--headless --path "${PROJECT_ROOT}" --script "${script}"
}
run_network_test() {
local script="$1"
local name="${script#tests/}"
name="${name%.gd}"
local host_root client_root host_pid host_status client_status
host_root="$(prepare_root "${name}-host")"
client_root="$(prepare_root "${name}-client")"
printf '\n==> %s (host + client)\n' "${script}"
XDG_DATA_HOME="${host_root}/data" \
XDG_CONFIG_HOME="${host_root}/config" \
XDG_CACHE_HOME="${host_root}/cache" \
timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
--headless --path "${PROJECT_ROOT}" --script "${script}" -- host \
>"${host_root}/output.log" 2>&1 &
host_pid=$!
sleep 0.75
set +e
XDG_DATA_HOME="${client_root}/data" \
XDG_CONFIG_HOME="${client_root}/config" \
XDG_CACHE_HOME="${client_root}/cache" \
timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
--headless --path "${PROJECT_ROOT}" --script "${script}" -- client \
>"${client_root}/output.log" 2>&1
client_status=$?
wait "${host_pid}"
host_status=$?
set -e
printf '%s\n' '-- host output --'
sed -n '1,240p' "${host_root}/output.log"
printf '%s\n' '-- client output --'
sed -n '1,240p' "${client_root}/output.log"
if ((host_status != 0 || client_status != 0)); then
printf 'error: host exited %d; client exited %d\n' \
"${host_status}" "${client_status}" >&2
return 1
fi
}
run_quick() {
local test_script
for test_script in "${QUICK_TESTS[@]}"; do
run_test "${test_script}"
done
}
run_full() {
local test_script
run_quick
for test_script in "${RUNTIME_TESTS[@]}"; do
run_test "${test_script}"
done
}
run_host() {
local test_script
for test_script in "${HOST_TESTS[@]}"; do
run_test "${test_script}"
done
}
run_network() {
local test_script
for test_script in "${NETWORK_TESTS[@]}"; do
run_network_test "${test_script}"
done
}
list_tests() {
printf '%s\n' 'Quick tests:'
printf ' %s\n' "${QUICK_TESTS[@]}"
printf '%s\n' 'Additional full-suite tests:'
printf ' %s\n' "${RUNTIME_TESTS[@]}"
printf '%s\n' 'Single-process tests requiring a local UDP bind:'
printf ' %s\n' "${HOST_TESTS[@]}"
printf '%s\n' 'Paired network tests:'
printf ' %s\n' "${NETWORK_TESTS[@]}"
}
case "${1:-}" in
quick)
run_quick
;;
full)
run_full
;;
host)
run_host
;;
network)
run_network
;;
all)
run_full
run_host
run_network
;;
--list)
list_tests
;;
*)
usage >&2
exit 2
;;
esac
printf '\nAll requested validations passed.\n'

View file

@ -48,7 +48,7 @@ func _run() -> void:
as PlayerAssetReservationService as PlayerAssetReservationService
) )
assert(player != null) assert(player != null)
assert(catalog != null and catalog.candidates.size() == 8) assert(catalog != null and catalog.candidates.size() == 19)
assert(sale_service != null) assert(sale_service != null)
assert(shop_service != null) assert(shop_service != null)
assert(session != null and session.is_host()) assert(session != null and session.is_host())

View file

@ -40,8 +40,22 @@ const CATFISH_IDS: Array[StringName] = [
const FRESH_WATER_IDS: Array[StringName] = [ const FRESH_WATER_IDS: Array[StringName] = [
&"bluegill", &"carp", &"catfish_blue", &"catfish_channel", &"bluegill", &"carp", &"catfish_blue", &"catfish_channel",
&"catfish_flathead", &"catfish_white", &"catfish_flathead", &"catfish_white",
&"goby_round",
]
const SALT_WATER_IDS: Array[StringName] = [
&"bass",
&"sunfish",
&"tuna_albacore",
&"tuna_bigeye",
&"tuna_bluefin",
&"tuna_skipjack",
&"tuna_yellowfin",
&"salmon_atlantic",
&"salmon_chum",
&"salmon_coho",
&"salmon_pink",
&"salmon_sockeye",
] ]
const SALT_WATER_IDS: Array[StringName] = [&"bass", &"sunfish"]
func _initialize() -> void: func _initialize() -> void:
@ -58,9 +72,9 @@ func _run() -> void:
func _validate_catalog_and_pools() -> void: func _validate_catalog_and_pools() -> void:
assert(Catalog.candidates.size() == 8) assert(Catalog.candidates.size() == 19)
assert(PondPool.candidates.size() == 6) assert(PondPool.candidates.size() == 7)
assert(OceanPool.candidates.size() == 2) assert(OceanPool.candidates.size() == 12)
for fish_id: StringName in ORIGINAL_IDS: for fish_id: StringName in ORIGINAL_IDS:
var original_fish: FishDataType = Catalog.get_fish_by_id(fish_id) var original_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
assert(original_fish != null) assert(original_fish != null)
@ -104,6 +118,43 @@ func _validate_catalog_and_pools() -> void:
assert(fish.sell_value_min == int(values[4])) assert(fish.sell_value_min == int(values[4]))
assert(fish.sell_value_max == int(values[5])) assert(fish.sell_value_max == int(values[5]))
var expected_tuna_values: Dictionary[StringName, Array] = {
&"tuna_albacore": [2, 1.5, 10.0, 45.0, 12, 24],
&"tuna_bigeye": [3, 0.55, 20.0, 120.0, 22, 48],
&"tuna_bluefin": [4, 0.18, 30.0, 250.0, 35, 90],
&"tuna_skipjack": [1, 3.0, 5.0, 25.0, 8, 16],
&"tuna_yellowfin": [2, 1.0, 15.0, 80.0, 16, 36],
}
for fish_id: StringName in expected_tuna_values:
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
var values: Array = expected_tuna_values[fish_id]
assert(fish != null and fish.is_selectable())
assert(int(fish.rarity) == int(values[0]))
assert(is_equal_approx(fish.base_catch_weight, float(values[1])))
assert(is_equal_approx(fish.weight_min_lb, float(values[2])))
assert(is_equal_approx(fish.weight_max_lb, float(values[3])))
assert(fish.sell_value_min == int(values[4]))
assert(fish.sell_value_max == int(values[5]))
var expected_new_values: Dictionary[StringName, Array] = {
&"goby_round": [0, 8.0, 0.1, 0.6, 2, 3],
&"salmon_atlantic": [3, 0.65, 8.0, 40.0, 20, 42],
&"salmon_chum": [1, 2.5, 6.0, 25.0, 10, 22],
&"salmon_coho": [2, 1.4, 5.0, 30.0, 14, 28],
&"salmon_pink": [1, 3.0, 3.0, 12.0, 8, 15],
&"salmon_sockeye": [2, 1.6, 4.0, 15.0, 12, 24],
}
for fish_id: StringName in expected_new_values:
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
var values: Array = expected_new_values[fish_id]
assert(fish != null and fish.is_selectable())
assert(int(fish.rarity) == int(values[0]))
assert(is_equal_approx(fish.base_catch_weight, float(values[1])))
assert(is_equal_approx(fish.weight_min_lb, float(values[2])))
assert(is_equal_approx(fish.weight_max_lb, float(values[3])))
assert(fish.sell_value_min == int(values[4]))
assert(fish.sell_value_max == int(values[5]))
var pond_context := FishingContextType.new() var pond_context := FishingContextType.new()
pond_context.location_tags = [&"starter_pond"] pond_context.location_tags = [&"starter_pond"]
pond_context.water_type = WaterType.Type.FRESH_WATER pond_context.water_type = WaterType.Type.FRESH_WATER

View file

@ -0,0 +1,54 @@
extends SceneTree
const FishingSpotScene := preload("res://fishing/fishing_spot.tscn")
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var fishing_spot := FishingSpotScene.instantiate() as FishingSpot
root.add_child(fishing_spot)
await process_frame
var fight_audio := fishing_spot.get_node("%FightAudio") as AudioStreamPlayer
var bobber_audio := fishing_spot.get_node(
"%BobberAudio"
) as AudioStreamPlayer
assert(bobber_audio != null)
assert(bobber_audio.stream != null)
assert(bobber_audio.bus == &"SFX")
assert(is_equal_approx(bobber_audio.volume_db, -14.0))
var bobber_stream := bobber_audio.stream as AudioStreamWAV
assert(bobber_stream != null)
assert(bobber_stream.loop_mode == AudioStreamWAV.LOOP_DISABLED)
assert(fight_audio != null)
assert(fight_audio.stream != null)
assert(fight_audio.bus == &"SFX")
assert(is_equal_approx(fight_audio.volume_db, -10.0))
var fight_stream := fight_audio.stream as AudioStreamWAV
assert(fight_stream != null)
assert(fight_stream.loop_mode == AudioStreamWAV.LOOP_FORWARD)
assert(fight_stream.loop_end > fight_stream.loop_begin)
var reeling_audio := fishing_spot.get_node(
"%ReelingAudio"
) as AudioStreamPlayer
assert(reeling_audio != null)
assert(reeling_audio.stream != null)
assert(reeling_audio.bus == &"SFX")
assert(is_equal_approx(reeling_audio.volume_db, -10.0))
var reeling_stream := reeling_audio.stream as AudioStreamWAV
assert(reeling_stream != null)
assert(reeling_stream.loop_mode == AudioStreamWAV.LOOP_FORWARD)
assert(reeling_stream.loop_end > reeling_stream.loop_begin)
assert(fishing_spot.has_method("_start_fight_audio"))
assert(fishing_spot.has_method("_play_bobber_audio"))
assert(fishing_spot.has_method("_stop_fight_audio"))
assert(fishing_spot.has_method("_start_reeling_audio"))
assert(fishing_spot.has_method("_stop_reeling_audio"))
fishing_spot.free()
await process_frame
print("Fishing audio validation: PASS")
quit()

View file

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

View file

@ -49,20 +49,20 @@ func _run() -> void:
assert(not hotbar.visible) assert(not hotbar.visible)
var entry_buttons: Dictionary = logbook.get("_entry_buttons") var entry_buttons: Dictionary = logbook.get("_entry_buttons")
assert(entry_buttons.size() == 6) assert(entry_buttons.size() == 7)
await _capture_if_requested("-unknown") await _capture_if_requested("-unknown")
logbook.call( logbook.call(
"_select_category", WaterType.Type.FRESH_WATER "_select_category", WaterType.Type.FRESH_WATER
) )
await create_timer(0.25).timeout await create_timer(0.25).timeout
assert((logbook.get("_entry_buttons") as Dictionary).size() == 6) assert((logbook.get("_entry_buttons") as Dictionary).size() == 7)
await _capture_if_requested("-fresh") await _capture_if_requested("-fresh")
logbook.call("_select_category", WaterType.Type.SALT_WATER) logbook.call("_select_category", WaterType.Type.SALT_WATER)
await create_timer(0.25).timeout await create_timer(0.25).timeout
assert((logbook.get("_entry_buttons") as Dictionary).size() == 2) assert((logbook.get("_entry_buttons") as Dictionary).size() == 12)
logbook.call("_select_category", WaterType.Type.FRESH_WATER) logbook.call("_select_category", WaterType.Type.FRESH_WATER)
await create_timer(0.25).timeout await create_timer(0.25).timeout
assert((logbook.get("_entry_buttons") as Dictionary).size() == 6) assert((logbook.get("_entry_buttons") as Dictionary).size() == 7)
player.collection_log.mark_discovered(&"bluegill") player.collection_log.mark_discovered(&"bluegill")
await process_frame await process_frame
logbook.call("_select_entry", &"bluegill", &"bluegill") logbook.call("_select_entry", &"bluegill", &"bluegill")
@ -101,7 +101,7 @@ func _validate_save_round_trip(
var player := main.get("_player") as Player var player := main.get("_player") as Player
var catalog := main.get("fish_catalog") as FishPool var catalog := main.get("fish_catalog") as FishPool
assert(catalog != null) assert(catalog != null)
assert(catalog.candidates.size() == 8) assert(catalog.candidates.size() == 19)
for index: int in 4: for index: int in 4:
_add_test_catch(player, catalog.candidates[index]) _add_test_catch(player, catalog.candidates[index])
assert(save_manager.save_now()) assert(save_manager.save_now())
@ -122,7 +122,7 @@ func _validate_save_round_trip(
assert(player.inventory.replace_all_catches(no_catches, 1)) assert(player.inventory.replace_all_catches(no_catches, 1))
assert(player.collection_log.replace_discovered_ids(no_discoveries)) assert(player.collection_log.replace_discovered_ids(no_discoveries))
assert(save_manager.load_player_data()) assert(save_manager.load_player_data())
assert(player.inventory.get_all_catches().size() == 8) assert(player.inventory.get_all_catches().size() == 19)
for fish: FishData in catalog.candidates: for fish: FishData in catalog.candidates:
assert(player.inventory.get_count(fish.id) == 1) assert(player.inventory.get_count(fish.id) == 1)
assert(player.collection_log.has_discovered(fish.id)) assert(player.collection_log.has_discovered(fish.id))

View file

@ -49,6 +49,17 @@ func _validate_catalog() -> void:
&"catfish_channel", &"catfish_channel",
&"catfish_flathead", &"catfish_flathead",
&"catfish_white", &"catfish_white",
&"tuna_albacore",
&"tuna_bigeye",
&"tuna_bluefin",
&"tuna_skipjack",
&"tuna_yellowfin",
&"goby_round",
&"salmon_atlantic",
&"salmon_chum",
&"salmon_coho",
&"salmon_pink",
&"salmon_sockeye",
] ]
assert(LogbookCatalog.CATALOG_ORDER == expected_ids) assert(LogbookCatalog.CATALOG_ORDER == expected_ids)
for index: int in expected_ids.size(): for index: int in expected_ids.size():
@ -62,7 +73,20 @@ func _validate_catalog() -> void:
) )
var expected_category: WaterType.Type = ( var expected_category: WaterType.Type = (
WaterType.Type.SALT_WATER WaterType.Type.SALT_WATER
if expected_ids[index] in [&"bass", &"sunfish"] if expected_ids[index] in [
&"bass",
&"sunfish",
&"tuna_albacore",
&"tuna_bigeye",
&"tuna_bluefin",
&"tuna_skipjack",
&"tuna_yellowfin",
&"salmon_atlantic",
&"salmon_chum",
&"salmon_coho",
&"salmon_pink",
&"salmon_sockeye",
]
else WaterType.Type.FRESH_WATER else WaterType.Type.FRESH_WATER
) )
assert( assert(
@ -105,7 +129,7 @@ func _validate_page() -> void:
page.call("_select_category", category) page.call("_select_category", category)
await create_timer(0.25).timeout await create_timer(0.25).timeout
var entries: Dictionary = page.get("_entry_buttons") var entries: Dictionary = page.get("_entry_buttons")
assert(entries.size() == (6 if category == WaterType.Type.FRESH_WATER else 2)) assert(entries.size() == (7 if category == WaterType.Type.FRESH_WATER else 12))
for fish: FishDataType in LogbookCatalog.ordered_species( for fish: FishDataType in LogbookCatalog.ordered_species(
CatalogResource.candidates CatalogResource.candidates
): ):
@ -155,7 +179,7 @@ func _validate_page() -> void:
candidate.display_name candidate.display_name
) )
) )
assert(silhouette_count == 8) assert(silhouette_count == 19)
page.call("_select_category", WaterType.Type.OTHER) page.call("_select_category", WaterType.Type.OTHER)
await create_timer(0.25).timeout await create_timer(0.25).timeout

View file

@ -0,0 +1,48 @@
extends SceneTree
const ShorelineAmbienceType := preload("res://world/shoreline_ambience.gd")
const WavesStream := preload("res://audio/ambience/waves.wav")
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var waves := WavesStream as AudioStreamWAV
assert(waves != null)
var segments: Array[PackedVector2Array] = [
PackedVector2Array([Vector2.ZERO, Vector2(10.0, 0.0)]),
]
assert(is_equal_approx(
ShorelineAmbienceType.distance_to_segments(Vector2(5.0, 3.0), segments),
3.0,
))
assert(is_equal_approx(
ShorelineAmbienceType.distance_to_segments(Vector2(-4.0, 0.0), segments),
4.0,
))
var controller := ShorelineAmbienceType.new() as ShorelineAmbience
var runtime_audio := AudioStreamPlayer.new()
runtime_audio.name = "WavesAudio"
runtime_audio.unique_name_in_owner = true
runtime_audio.stream = WavesStream
runtime_audio.bus = &"SFX"
controller.add_child(runtime_audio)
runtime_audio.owner = controller
root.add_child(controller)
await process_frame
assert(runtime_audio.bus == &"SFX")
assert(controller.near_distance < controller.far_distance)
assert(controller.near_volume_db > controller.far_volume_db)
var runtime_waves := runtime_audio.stream as AudioStreamWAV
assert(runtime_waves.loop_mode == AudioStreamWAV.LOOP_FORWARD)
assert(runtime_waves.loop_begin == 0)
assert(runtime_waves.loop_end == 1044956)
controller.free()
await process_frame
print("Shoreline ambience validation: PASS")
quit()

View file

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

View file

@ -17,6 +17,12 @@ const FishableWaterRegionType = preload(
const WeatherIconType = preload("res://ui/weather_icon.gd") const WeatherIconType = preload("res://ui/weather_icon.gd")
const Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres") const Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres")
const SaltWaterMaterial: ShaderMaterial = preload(
"res://world/materials/stylized_water.tres"
)
const FreshWaterMaterial: ShaderMaterial = preload(
"res://world/materials/stylized_water_fresh.tres"
)
func _initialize() -> void: func _initialize() -> void:
@ -138,7 +144,21 @@ func _validate_fishing_availability() -> void:
assert(not night_fish.availability.is_available(day_context)) assert(not night_fish.availability.is_available(day_context))
assert(night_fish.availability.is_available(night_context)) assert(night_fish.availability.is_available(night_context))
assert(night_fish.availability.is_available(transition_context)) assert(night_fish.availability.is_available(transition_context))
for fish_id: StringName in [&"bass", &"carp"]: for fish_id: StringName in [
&"bass",
&"carp",
&"tuna_albacore",
&"tuna_bigeye",
&"tuna_bluefin",
&"tuna_skipjack",
&"tuna_yellowfin",
&"goby_round",
&"salmon_atlantic",
&"salmon_chum",
&"salmon_coho",
&"salmon_pink",
&"salmon_sockeye",
]:
var all_time_fish: FishDataType = Catalog.get_fish_by_id(fish_id) var all_time_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
assert(all_time_fish.availability.is_available(day_context)) assert(all_time_fish.availability.is_available(day_context))
assert(all_time_fish.availability.is_available(night_context)) assert(all_time_fish.availability.is_available(night_context))
@ -229,6 +249,14 @@ func _validate_environment_presentation() -> void:
< 0.01 < 0.01
) )
var day_ambient_energy: float = runtime_environment.ambient_light_energy var day_ambient_energy: float = runtime_environment.ambient_light_energy
var day_water_brightness := float(
SaltWaterMaterial.get_shader_parameter("environment_brightness")
)
assert(day_water_brightness > 0.99)
assert(is_equal_approx(
float(FreshWaterMaterial.get_shader_parameter("environment_brightness")),
day_water_brightness,
))
visuals.apply_time_immediately(0.0) visuals.apply_time_immediately(0.0)
var night_horizon: Color = runtime_sky_material.get_shader_parameter( var night_horizon: Color = runtime_sky_material.get_shader_parameter(
"sky_horizon_color" "sky_horizon_color"
@ -251,6 +279,47 @@ func _validate_environment_presentation() -> void:
assert(night_moon_direction.y > 0.85) assert(night_moon_direction.y > 0.85)
assert(night_moon_direction.is_equal_approx(-night_sun_direction)) assert(night_moon_direction.is_equal_approx(-night_sun_direction))
assert(runtime_environment.ambient_light_energy < day_ambient_energy) assert(runtime_environment.ambient_light_energy < day_ambient_energy)
var night_water_brightness := float(
SaltWaterMaterial.get_shader_parameter("environment_brightness")
)
assert(night_water_brightness < day_water_brightness * 0.4)
assert(is_equal_approx(
float(FreshWaterMaterial.get_shader_parameter("environment_brightness")),
night_water_brightness,
))
var night_water_tint := (
SaltWaterMaterial.get_shader_parameter("environment_tint") as Color
)
assert(night_water_tint.get_luminance() < 0.5)
var night_background_energy := runtime_environment.background_energy_multiplier
var night_adjustment_brightness := runtime_environment.adjustment_brightness
var night_adjustment_saturation := runtime_environment.adjustment_saturation
visuals.apply_weather_immediately(WorldWeatherService.Weather.FOGGY)
visuals.apply_time_immediately(0.0)
var foggy_sky_horizon := (
runtime_sky_material.get_shader_parameter("sky_horizon_color") as Color
)
assert(foggy_sky_horizon.is_equal_approx(night_horizon))
assert(is_zero_approx(runtime_environment.fog_sky_affect))
assert(is_equal_approx(runtime_environment.fog_depth_begin, 3.0))
assert(is_equal_approx(runtime_environment.fog_depth_end, 28.0))
assert(is_equal_approx(
float(SaltWaterMaterial.get_shader_parameter("environment_brightness")),
night_water_brightness,
))
assert(is_equal_approx(
runtime_environment.background_energy_multiplier,
night_background_energy,
))
assert(is_equal_approx(
runtime_environment.adjustment_brightness,
night_adjustment_brightness,
))
assert(is_equal_approx(
runtime_environment.adjustment_saturation,
night_adjustment_saturation,
))
visuals.apply_weather_immediately(WorldWeatherService.Weather.SUNNY)
visuals.apply_time_immediately(20.0) visuals.apply_time_immediately(20.0)
var dusk_horizon: Color = runtime_sky_material.get_shader_parameter( var dusk_horizon: Color = runtime_sky_material.get_shader_parameter(
"sky_horizon_color" "sky_horizon_color"

View file

@ -14,6 +14,17 @@ const CATALOG_ORDER: Array[StringName] = [
&"catfish_channel", &"catfish_channel",
&"catfish_flathead", &"catfish_flathead",
&"catfish_white", &"catfish_white",
&"tuna_albacore",
&"tuna_bigeye",
&"tuna_bluefin",
&"tuna_skipjack",
&"tuna_yellowfin",
&"goby_round",
&"salmon_atlantic",
&"salmon_chum",
&"salmon_coho",
&"salmon_pink",
&"salmon_sockeye",
] ]
# Short presentation notes are kept here with the catalog contract rather # Short presentation notes are kept here with the catalog contract rather
@ -52,6 +63,50 @@ const FISH_FACTS: Dictionary[StringName, String] = {
"white catfish began along the atlantic coast between new york " "white catfish began along the atlantic coast between new york "
+ "and florida. people later carried them far beyond that home range." + "and florida. people later carried them far beyond that home range."
), ),
&"tuna_albacore": (
"albacore have unusually long pectoral fins. their warm muscles help "
+ "them cruise across wide stretches of open ocean."
),
&"tuna_bigeye": (
"bigeye tuna make daily trips between deep, cool water and warmer "
+ "surface layers. their large eyes suit those dim depths."
),
&"tuna_bluefin": (
"bluefin tuna are powerful ocean travelers. heat retained in their "
+ "swimming muscles lets them thrive in surprisingly cold seas."
),
&"tuna_skipjack": (
"skipjack travel in fast-moving schools and have dark stripes along "
+ "their lower sides. they rarely stop swimming."
),
&"tuna_yellowfin": (
"yellowfin are named for their bright yellow fins and finlets. large "
+ "schools often gather with other open-ocean hunters."
),
&"goby_round": (
"round gobies use fused pelvic fins like a suction cup to hold onto "
+ "rocks. they are small fish with a famously large appetite."
),
&"salmon_atlantic": (
"atlantic salmon can return from the ocean to the river where they "
+ "hatched. unlike many salmon, some survive to make the trip again."
),
&"salmon_chum": (
"chum salmon travel immense distances and develop striking bars when "
+ "they return to fresh water to spawn."
),
&"salmon_coho": (
"coho salmon are agile hunters known for powerful leaps. young coho "
+ "may spend more than a year growing in streams."
),
&"salmon_pink": (
"pink salmon usually follow a strict two-year life cycle. spawning "
+ "males develop the hump that inspired their nickname."
),
&"salmon_sockeye": (
"sockeye salmon feed heavily on plankton at sea. adults turn vivid red "
+ "as they return inland to spawn."
),
} }
static func category_for(fish: FishDataType) -> WaterType.Type: static func category_for(fish: FishDataType) -> WaterType.Type:

View file

@ -4,6 +4,7 @@ uniform vec4 sky_top_color : source_color = vec4(0.204, 0.498, 0.643, 1.0);
uniform vec4 sky_horizon_color : source_color = vec4(0.663, 0.843, 0.847, 1.0); uniform vec4 sky_horizon_color : source_color = vec4(0.663, 0.843, 0.847, 1.0);
uniform vec4 ground_bottom_color : source_color = vec4(0.157, 0.361, 0.439, 1.0); uniform vec4 ground_bottom_color : source_color = vec4(0.157, 0.361, 0.439, 1.0);
uniform vec4 ground_horizon_color : source_color = vec4(0.549, 0.749, 0.765, 1.0); uniform vec4 ground_horizon_color : source_color = vec4(0.549, 0.749, 0.765, 1.0);
uniform float horizon_blend_width : hint_range(0.005, 0.15, 0.005) = 0.01;
uniform vec3 sun_direction = vec3(0.0, 1.0, 0.0); uniform vec3 sun_direction = vec3(0.0, 1.0, 0.0);
uniform vec4 sun_color : source_color = vec4(1.0, 0.86, 0.46, 1.0); uniform vec4 sun_color : source_color = vec4(1.0, 0.86, 0.46, 1.0);
@ -39,7 +40,11 @@ void sky() {
vec3 base_color = mix( vec3 base_color = mix(
ground_color, ground_color,
sky_color, sky_color,
smoothstep(-0.01, 0.01, view_direction.y) smoothstep(
-horizon_blend_width,
horizon_blend_width,
view_direction.y
)
); );
float alignment = dot(view_direction, normalize(sun_direction)); float alignment = dot(view_direction, normalize(sun_direction));

View file

@ -1,5 +1,5 @@
shader_type spatial; shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_back, unshaded; render_mode blend_mix, depth_draw_never, cull_back, unshaded, fog_disabled;
uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest; uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;
@ -35,6 +35,8 @@ uniform float open_water_drift_strength : hint_range(0.0, 0.08, 0.001) = 0.045;
uniform float distant_alpha_start : hint_range(10.0, 2000.0, 1.0) = 260.0; uniform float distant_alpha_start : hint_range(10.0, 2000.0, 1.0) = 260.0;
uniform float distant_alpha_end : hint_range(20.0, 5000.0, 1.0) = 1400.0; uniform float distant_alpha_end : hint_range(20.0, 5000.0, 1.0) = 1400.0;
uniform vec4 environment_tint : source_color = vec4(1.0);
uniform float environment_brightness : hint_range(0.1, 1.2, 0.01) = 1.0;
varying vec3 world_position; varying vec3 world_position;
varying float surface_view_depth; varying float surface_view_depth;
@ -233,6 +235,8 @@ void fragment() {
water_color = mix(water_color, deep_color.rgb, distant_mix * 0.72); water_color = mix(water_color, deep_color.rgb, distant_mix * 0.72);
water_alpha = mix(water_alpha, 0.96, distant_mix); water_alpha = mix(water_alpha, 0.96, distant_mix);
water_color *= environment_tint.rgb * environment_brightness;
ALBEDO = water_color; ALBEDO = water_color;
ALPHA = clamp(water_alpha, 0.1, 0.96); ALPHA = clamp(water_alpha, 0.1, 0.96);
METALLIC = 0.0; METALLIC = 0.0;

View file

@ -32,3 +32,5 @@ shader_parameter/open_water_drift_speed = Vector2(0.01, -0.007)
shader_parameter/open_water_drift_strength = 0.045 shader_parameter/open_water_drift_strength = 0.045
shader_parameter/distant_alpha_start = 260.0 shader_parameter/distant_alpha_start = 260.0
shader_parameter/distant_alpha_end = 1400.0 shader_parameter/distant_alpha_end = 1400.0
shader_parameter/environment_tint = Color(1, 1, 1, 1)
shader_parameter/environment_brightness = 1.0

View file

@ -21,3 +21,5 @@ shader_parameter/open_water_drift_speed = Vector2(0.01, -0.007)
shader_parameter/open_water_drift_strength = 0.045 shader_parameter/open_water_drift_strength = 0.045
shader_parameter/distant_alpha_start = 260.0 shader_parameter/distant_alpha_start = 260.0
shader_parameter/distant_alpha_end = 1400.0 shader_parameter/distant_alpha_end = 1400.0
shader_parameter/environment_tint = Color(1, 1, 1, 1)
shader_parameter/environment_brightness = 1.0

View file

@ -58,6 +58,10 @@ func get_pelican_landmark() -> Node3D:
return get_node_or_null(pelican_landmark_path) as Node3D return get_node_or_null(pelican_landmark_path) as Node3D
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
func has_terrain_collision() -> bool: func has_terrain_collision() -> bool:
var collision_shape: CollisionShape3D = ( var collision_shape: CollisionShape3D = (
get_node_or_null(terrain_collision_shape_path) as CollisionShape3D get_node_or_null(terrain_collision_shape_path) as CollisionShape3D

156
world/shoreline_ambience.gd Normal file
View file

@ -0,0 +1,156 @@
class_name ShorelineAmbience
extends Node
@export_range(0.0, 20.0, 0.1) var near_distance: float = 2.0
@export_range(1.0, 100.0, 0.5) var far_distance: float = 24.0
@export_range(-40.0, 12.0, 0.5) var near_volume_db: float = 1.0
@export_range(-80.0, 0.0, 0.5) var far_volume_db: float = -18.0
@export_range(0.05, 1.0, 0.05) var distance_update_interval: float = 0.2
@export_range(0.1, 20.0, 0.1) var volume_smoothing_speed: float = 4.0
@onready var _waves_audio: AudioStreamPlayer = %WavesAudio
var _listener: Node3D
var _shoreline_segments: Array[PackedVector2Array] = []
var _distance_update_remaining: float = 0.0
var _target_volume_db: float = -80.0
var _is_active := false
func _ready() -> void:
_configure_audio_loop(_waves_audio.stream)
_waves_audio.volume_db = -80.0
set_process(false)
func configure(listener: Node3D, shoreline_mesh: MeshInstance3D) -> void:
_listener = listener
_shoreline_segments = _extract_shoreline_segments(shoreline_mesh)
_distance_update_remaining = 0.0
if _shoreline_segments.is_empty():
push_warning("Saltwater shoreline ambience has no coastline geometry.")
func set_active(active: bool) -> void:
_is_active = active
set_process(active)
if active:
_distance_update_remaining = 0.0
_update_target_volume()
_waves_audio.volume_db = _target_volume_db
if not _waves_audio.playing and _waves_audio.stream != null:
_waves_audio.play()
else:
_waves_audio.stop()
_waves_audio.volume_db = -80.0
func _process(delta: float) -> void:
if not _is_active:
return
_distance_update_remaining -= delta
if _distance_update_remaining <= 0.0:
_distance_update_remaining = distance_update_interval
_update_target_volume()
_waves_audio.volume_db = lerpf(
_waves_audio.volume_db,
_target_volume_db,
1.0 - exp(-volume_smoothing_speed * delta),
)
func _update_target_volume() -> void:
if _listener == null or _shoreline_segments.is_empty():
_target_volume_db = -80.0
return
var listener_position := Vector2(
_listener.global_position.x,
_listener.global_position.z,
)
var distance := distance_to_segments(listener_position, _shoreline_segments)
var blend := clampf(
inverse_lerp(near_distance, maxf(near_distance + 0.01, far_distance), distance),
0.0,
1.0,
)
_target_volume_db = lerpf(near_volume_db, far_volume_db, smoothstep(0.0, 1.0, blend))
func _extract_shoreline_segments(
shoreline_mesh: MeshInstance3D,
) -> Array[PackedVector2Array]:
var segments: Array[PackedVector2Array] = []
var seen_segments: Dictionary = {}
if shoreline_mesh == null or shoreline_mesh.mesh == null:
return segments
for surface_index in shoreline_mesh.mesh.get_surface_count():
var arrays := shoreline_mesh.mesh.surface_get_arrays(surface_index)
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
if indices.is_empty():
continue
for index_offset in range(0, indices.size() - 2, 3):
var triangle := PackedVector2Array()
for corner in 3:
var vertex := shoreline_mesh.to_global(
vertices[indices[index_offset + corner]]
)
triangle.append(Vector2(vertex.x, vertex.z))
_append_unique_segment(segments, seen_segments, triangle[0], triangle[1])
_append_unique_segment(segments, seen_segments, triangle[1], triangle[2])
_append_unique_segment(segments, seen_segments, triangle[2], triangle[0])
return segments
func _append_unique_segment(
segments: Array[PackedVector2Array],
seen_segments: Dictionary,
start: Vector2,
end: Vector2,
) -> void:
if start.distance_squared_to(end) <= 0.000001:
return
# The baked ribbon repeats shared triangle edges. Quantized canonical keys
# keep only distinct segments without an O(n²) startup pass.
var first := start
var second := end
if first.x > second.x or (is_equal_approx(first.x, second.x) and first.y > second.y):
first = end
second = start
var key := "%d,%d:%d,%d" % [
roundi(first.x * 10000.0),
roundi(first.y * 10000.0),
roundi(second.x * 10000.0),
roundi(second.y * 10000.0),
]
if seen_segments.has(key):
return
seen_segments[key] = true
segments.append(PackedVector2Array([start, end]))
static func distance_to_segments(
point: Vector2,
segments: Array[PackedVector2Array],
) -> float:
var closest_squared := INF
for segment in segments:
if segment.size() < 2:
continue
var closest := Geometry2D.get_closest_point_to_segment(
point,
segment[0],
segment[1],
)
closest_squared = minf(closest_squared, point.distance_squared_to(closest))
return sqrt(closest_squared) if closest_squared < INF else INF
func _configure_audio_loop(stream: AudioStream) -> void:
var wav := stream as AudioStreamWAV
if wav == null:
return
wav.loop_mode = AudioStreamWAV.LOOP_FORWARD
if wav.loop_end <= wav.loop_begin:
wav.loop_begin = 0
wav.loop_end = int(round(wav.get_length() * wav.mix_rate))

View file

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

View file

@ -61,6 +61,10 @@ func get_pelican_convenience_landmark() -> Node3D:
return _starter_island.get_pelican_landmark() return _starter_island.get_pelican_landmark()
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
return _starter_island.get_saltwater_shoreline_mesh()
func _get_regions() -> Array[WorldRegion]: func _get_regions() -> Array[WorldRegion]:
var regions: Array[WorldRegion] = [] var regions: Array[WorldRegion] = []
for child: Node in _regions_root.get_children(): for child: Node in _regions_root.get_children():

View file

@ -9,6 +9,12 @@ const RAIN_PARTICLE_AMOUNT: int = 560
const RAIN_VELOCITY_MIN: float = 16.0 const RAIN_VELOCITY_MIN: float = 16.0
const RAIN_VELOCITY_MAX: float = 20.0 const RAIN_VELOCITY_MAX: float = 20.0
const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014) const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014)
const SALT_WATER_MATERIAL: ShaderMaterial = preload(
"res://world/materials/stylized_water.tres"
)
const FRESH_WATER_MATERIAL: ShaderMaterial = preload(
"res://world/materials/stylized_water_fresh.tres"
)
const DAY_SKY_TOP := Color(0.204, 0.498, 0.643) const DAY_SKY_TOP := Color(0.204, 0.498, 0.643)
const DAY_SKY_HORIZON := Color(0.663, 0.843, 0.847) const DAY_SKY_HORIZON := Color(0.663, 0.843, 0.847)
@ -26,6 +32,7 @@ const NIGHT_GROUND_HORIZON := Color(0.075, 0.135, 0.22)
const NIGHT_AMBIENT := Color(0.28, 0.35, 0.48) const NIGHT_AMBIENT := Color(0.28, 0.35, 0.48)
const NIGHT_FOG := Color(0.13, 0.21, 0.32) const NIGHT_FOG := Color(0.13, 0.21, 0.32)
const NIGHT_SUN := Color(0.35, 0.44, 0.62) const NIGHT_SUN := Color(0.35, 0.44, 0.62)
const NIGHT_WATER_TINT := Color(0.34, 0.48, 0.58)
const WARM_SKY_TOP := Color(0.31, 0.30, 0.42) const WARM_SKY_TOP := Color(0.31, 0.30, 0.42)
const WARM_SKY_HORIZON := Color(0.96, 0.48, 0.22) const WARM_SKY_HORIZON := Color(0.96, 0.48, 0.22)
@ -34,13 +41,10 @@ const WARM_GROUND_HORIZON := Color(0.82, 0.34, 0.18)
const WARM_AMBIENT := Color(0.92, 0.58, 0.40) const WARM_AMBIENT := Color(0.92, 0.58, 0.40)
const WARM_FOG := Color(0.74, 0.39, 0.27) const WARM_FOG := Color(0.74, 0.39, 0.27)
const WARM_SUN := Color(1.0, 0.58, 0.30) const WARM_SUN := Color(1.0, 0.58, 0.30)
const WARM_WATER_TINT := Color(0.78, 0.62, 0.58)
const WARM_SUN_DISC := Color(1.0, 0.45, 0.16) const WARM_SUN_DISC := Color(1.0, 0.45, 0.16)
const MOON_DISC := Color(0.78, 0.88, 1.0) const MOON_DISC := Color(0.78, 0.88, 1.0)
const CLOUDY_TINT := Color(0.53, 0.61, 0.66)
const RAINY_TINT := Color(0.31, 0.42, 0.50)
const FOGGY_TINT := Color(0.62, 0.70, 0.72)
var _time_service: WorldTimeService var _time_service: WorldTimeService
var _weather_service: WorldWeatherService var _weather_service: WorldWeatherService
var _world_environment: WorldEnvironment var _world_environment: WorldEnvironment
@ -216,38 +220,31 @@ func _apply_time(time_hours: float) -> void:
var fog_color: Color = _blended_color( var fog_color: Color = _blended_color(
NIGHT_FOG, DAY_FOG, WARM_FOG, daylight, warmth NIGHT_FOG, DAY_FOG, WARM_FOG, daylight, warmth
) )
var weather_tint: Color = _weather_color() _sky_material.set_shader_parameter(
var tint_strength: float = _weather_value( "sky_top_color", sky_top
0.0, 0.30, 0.48, 0.62
) )
_sky_material.set_shader_parameter( _sky_material.set_shader_parameter(
"sky_top_color", sky_top.lerp(weather_tint, tint_strength) "sky_horizon_color",
) sky_horizon,
_sky_material.set_shader_parameter(
"sky_horizon_color", sky_horizon.lerp(weather_tint, tint_strength)
) )
_sky_material.set_shader_parameter( _sky_material.set_shader_parameter(
"ground_bottom_color", "ground_bottom_color",
ground_bottom.lerp(weather_tint, tint_strength * 0.62), ground_bottom,
) )
_sky_material.set_shader_parameter( _sky_material.set_shader_parameter(
"ground_horizon_color", "ground_horizon_color",
ground_horizon.lerp(weather_tint, tint_strength * 0.76), ground_horizon,
) )
_environment.background_energy_multiplier = ( _environment.background_energy_multiplier = (
lerpf(0.52, 0.85, daylight) lerpf(0.52, 0.85, daylight)
* _weather_value(1.0, 0.82, 0.66, 0.74) * _weather_value(1.0, 0.82, 0.66, 1.0)
)
_environment.ambient_light_color = ambient.lerp(
weather_tint, tint_strength * 0.45
) )
_environment.ambient_light_color = ambient
_environment.ambient_light_energy = ( _environment.ambient_light_energy = (
lerpf(0.66, 1.08, daylight) lerpf(0.66, 1.08, daylight)
* _weather_value(1.0, 0.88, 0.76, 0.82) * _weather_value(1.0, 0.88, 0.76, 0.82)
) )
_environment.fog_light_color = fog_color.lerp( _environment.fog_light_color = fog_color
weather_tint, tint_strength * 0.82
)
_environment.fog_light_energy = ( _environment.fog_light_energy = (
lerpf(0.56, 0.82, daylight) lerpf(0.56, 0.82, daylight)
* _weather_value(1.0, 0.92, 0.82, 1.05) * _weather_value(1.0, 0.92, 0.82, 1.05)
@ -255,25 +252,24 @@ func _apply_time(time_hours: float) -> void:
_environment.fog_aerial_perspective = _weather_value( _environment.fog_aerial_perspective = _weather_value(
0.35, 0.48, 0.62, 0.92 0.35, 0.48, 0.62, 0.92
) )
_environment.fog_sky_affect = _weather_value( _environment.fog_sky_affect = 0.0
0.35, 0.48, 0.68, 0.94
)
_environment.fog_depth_curve = _weather_value( _environment.fog_depth_curve = _weather_value(
1.6, 1.5, 1.4, 1.18 1.6, 1.5, 1.4, 1.18
) )
_environment.fog_depth_begin = _weather_value( _environment.fog_depth_begin = _weather_value(
42.0, 32.0, 20.0, 4.0 42.0, 32.0, 20.0, 3.0
) )
_environment.fog_depth_end = _weather_value( _environment.fog_depth_end = _weather_value(
170.0, 140.0, 95.0, 42.0 170.0, 140.0, 95.0, 28.0
) )
_apply_water_environment(daylight, warmth)
_environment.adjustment_brightness = ( _environment.adjustment_brightness = (
lerpf(0.93, 0.98, daylight) lerpf(0.93, 0.98, daylight)
* _weather_value(1.0, 0.97, 0.92, 0.96) * _weather_value(1.0, 0.97, 0.92, 1.0)
) )
_environment.adjustment_saturation = ( _environment.adjustment_saturation = (
lerpf(0.82, 0.91, daylight) lerpf(0.82, 0.91, daylight)
* _weather_value(1.0, 0.88, 0.78, 0.70) * _weather_value(1.0, 0.88, 0.78, 1.0)
) )
_sun.rotation_degrees = Vector3( _sun.rotation_degrees = Vector3(
-360.0 * fposmod(hour - WorldTimeService.DAY_START_HOUR, 24.0) / 24.0, -360.0 * fposmod(hour - WorldTimeService.DAY_START_HOUR, 24.0) / 24.0,
@ -288,7 +284,7 @@ func _apply_time(time_hours: float) -> void:
) )
_sky_material.set_shader_parameter( _sky_material.set_shader_parameter(
"sun_visibility", "sun_visibility",
daylight * _weather_value(1.0, 0.65, 0.28, 0.15), daylight * _weather_value(1.0, 0.65, 0.28, 1.0),
) )
_sky_material.set_shader_parameter( _sky_material.set_shader_parameter(
"moon_direction", -_sun.global_transform.basis.z.normalized() "moon_direction", -_sun.global_transform.basis.z.normalized()
@ -296,7 +292,7 @@ func _apply_time(time_hours: float) -> void:
_sky_material.set_shader_parameter("moon_color", MOON_DISC) _sky_material.set_shader_parameter("moon_color", MOON_DISC)
_sky_material.set_shader_parameter( _sky_material.set_shader_parameter(
"moon_visibility", "moon_visibility",
(1.0 - daylight) * _weather_value(1.0, 0.72, 0.36, 0.20), (1.0 - daylight) * _weather_value(1.0, 0.72, 0.36, 1.0),
) )
_sun.light_color = _blended_color( _sun.light_color = _blended_color(
NIGHT_SUN, DAY_SUN, WARM_SUN, daylight, warmth NIGHT_SUN, DAY_SUN, WARM_SUN, daylight, warmth
@ -308,6 +304,32 @@ func _apply_time(time_hours: float) -> void:
_update_rain_amount() _update_rain_amount()
func _apply_water_environment(
daylight: float,
warmth: float,
) -> void:
var water_tint := _blended_color(
NIGHT_WATER_TINT,
Color.WHITE,
WARM_WATER_TINT,
daylight,
warmth,
)
var water_brightness := (
lerpf(0.34, 1.0, daylight)
* _weather_value(1.0, 0.90, 0.78, 1.0)
)
for material: ShaderMaterial in [
SALT_WATER_MATERIAL,
FRESH_WATER_MATERIAL,
]:
material.set_shader_parameter("environment_tint", water_tint)
material.set_shader_parameter(
"environment_brightness",
water_brightness,
)
func _on_weather_changed( func _on_weather_changed(
weather: WorldWeatherService.Weather, weather: WorldWeatherService.Weather,
_seconds_remaining: float, _seconds_remaining: float,
@ -349,25 +371,6 @@ func _weather_state_value(
return sunny return sunny
func _weather_color() -> Color:
return _weather_state_color(_weather_from).lerp(
_weather_state_color(_weather_to), _weather_transition
)
func _weather_state_color(
weather: WorldWeatherService.Weather,
) -> Color:
match weather:
WorldWeatherService.Weather.CLOUDY:
return CLOUDY_TINT
WorldWeatherService.Weather.RAINY:
return RAINY_TINT
WorldWeatherService.Weather.FOGGY:
return FOGGY_TINT
return Color.WHITE
func _update_rain_amount() -> void: func _update_rain_amount() -> void:
if _rain == null: if _rain == null:
return return