diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8406c0a --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..8edbd4f --- /dev/null +++ b/CREDITS.md @@ -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). diff --git a/README.md b/README.md new file mode 100644 index 0000000..af29cbc --- /dev/null +++ b/README.md @@ -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. diff --git a/audio/ambience/waves.wav b/audio/ambience/waves.wav new file mode 100644 index 0000000..15f258e Binary files /dev/null and b/audio/ambience/waves.wav differ diff --git a/audio/ambience/waves.wav.import b/audio/ambience/waves.wav.import new file mode 100644 index 0000000..5ff6ba8 --- /dev/null +++ b/audio/ambience/waves.wav.import @@ -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 diff --git a/audio/sfx/fishing/bobber.wav b/audio/sfx/fishing/bobber.wav new file mode 100644 index 0000000..8c9d916 Binary files /dev/null and b/audio/sfx/fishing/bobber.wav differ diff --git a/audio/sfx/fishing/bobber.wav.import b/audio/sfx/fishing/bobber.wav.import new file mode 100644 index 0000000..d5fc82d --- /dev/null +++ b/audio/sfx/fishing/bobber.wav.import @@ -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 diff --git a/audio/sfx/fishing/fighting.wav b/audio/sfx/fishing/fighting.wav new file mode 100644 index 0000000..f647069 Binary files /dev/null and b/audio/sfx/fishing/fighting.wav differ diff --git a/audio/sfx/fishing/fighting.wav.import b/audio/sfx/fishing/fighting.wav.import new file mode 100644 index 0000000..ec59561 --- /dev/null +++ b/audio/sfx/fishing/fighting.wav.import @@ -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 diff --git a/audio/sfx/fishing/reeling.wav b/audio/sfx/fishing/reeling.wav new file mode 100644 index 0000000..6be5d03 Binary files /dev/null and b/audio/sfx/fishing/reeling.wav differ diff --git a/audio/sfx/fishing/reeling.wav.import b/audio/sfx/fishing/reeling.wav.import new file mode 100644 index 0000000..e5e6749 --- /dev/null +++ b/audio/sfx/fishing/reeling.wav.import @@ -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 diff --git a/default_bus_layout.tres b/default_bus_layout.tres index ca38684..665b0e9 100644 --- a/default_bus_layout.tres +++ b/default_bus_layout.tres @@ -13,3 +13,9 @@ bus/1/mute = false bus/1/bypass_fx = false bus/1/volume_db = 0.0 bus/1/send = &"Master" +bus/2/name = &"SFX" +bus/2/solo = false +bus/2/mute = false +bus/2/bypass_fx = false +bus/2/volume_db = 0.0 +bus/2/send = &"Master" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..373c800 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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). diff --git a/docs/ASSET-PROVENANCE.md b/docs/ASSET-PROVENANCE.md new file mode 100644 index 0000000..9c3bc76 --- /dev/null +++ b/docs/ASSET-PROVENANCE.md @@ -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. diff --git a/docs/BALANCE.md b/docs/BALANCE.md new file mode 100644 index 0000000..948b882 --- /dev/null +++ b/docs/BALANCE.md @@ -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. diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..e70455b --- /dev/null +++ b/docs/TESTING.md @@ -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. diff --git a/docs/decisions/0001-host-authority.md b/docs/decisions/0001-host-authority.md new file mode 100644 index 0000000..f134d93 --- /dev/null +++ b/docs/decisions/0001-host-authority.md @@ -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. diff --git a/docs/decisions/0002-authored-data-and-stable-ids.md b/docs/decisions/0002-authored-data-and-stable-ids.md new file mode 100644 index 0000000..23163cb --- /dev/null +++ b/docs/decisions/0002-authored-data-and-stable-ids.md @@ -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. diff --git a/fish/pools/fish_catalog.tres b/fish/pools/fish_catalog.tres index 66a1c34..d646449 100644 --- a/fish/pools/fish_catalog.tres +++ b/fish/pools/fish_catalog.tres @@ -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="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_flathead/catfish_flathead.tres" id="8_catfish_flathead"] [ext_resource type="Resource" path="res://fish/species/catfish_white/catfish_white.tres" id="9_catfish_white"] +[ext_resource type="Resource" path="res://fish/species/tuna_albacore/tuna_albacore.tres" id="10_tuna_albacore"] +[ext_resource type="Resource" path="res://fish/species/tuna_bigeye/tuna_bigeye.tres" id="11_tuna_bigeye"] +[ext_resource type="Resource" path="res://fish/species/tuna_bluefin/tuna_bluefin.tres" id="12_tuna_bluefin"] +[ext_resource type="Resource" path="res://fish/species/tuna_skipjack/tuna_skipjack.tres" id="13_tuna_skipjack"] +[ext_resource type="Resource" path="res://fish/species/tuna_yellowfin/tuna_yellowfin.tres" id="14_tuna_yellowfin"] +[ext_resource type="Resource" path="res://fish/species/goby_round/goby_round.tres" id="15_goby_round"] +[ext_resource type="Resource" path="res://fish/species/salmon_atlantic/salmon_atlantic.tres" id="16_salmon_atlantic"] +[ext_resource type="Resource" path="res://fish/species/salmon_chum/salmon_chum.tres" id="17_salmon_chum"] +[ext_resource type="Resource" path="res://fish/species/salmon_coho/salmon_coho.tres" id="18_salmon_coho"] +[ext_resource type="Resource" path="res://fish/species/salmon_pink/salmon_pink.tres" id="19_salmon_pink"] +[ext_resource type="Resource" path="res://fish/species/salmon_sockeye/salmon_sockeye.tres" id="20_salmon_sockeye"] [resource] script = ExtResource("1_pool") -candidates = [ExtResource("2_bluegill"), ExtResource("3_bass"), ExtResource("4_carp"), ExtResource("5_sunfish"), ExtResource("6_catfish_blue"), ExtResource("7_catfish_channel"), ExtResource("8_catfish_flathead"), ExtResource("9_catfish_white")] +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")] diff --git a/fish/pools/starter_ocean_pool.tres b/fish/pools/starter_ocean_pool.tres index a84106e..8772d68 100644 --- a/fish/pools/starter_ocean_pool.tres +++ b/fish/pools/starter_ocean_pool.tres @@ -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="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/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] 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")] diff --git a/fish/pools/starter_pond_pool.tres b/fish/pools/starter_pond_pool.tres index a594d72..276aaa5 100644 --- a/fish/pools/starter_pond_pool.tres +++ b/fish/pools/starter_pond_pool.tres @@ -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="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_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/goby_round/goby_round.tres" id="10_goby_round"] [resource] 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")] diff --git a/fish/species/goby_round/fish_goby_round.png b/fish/species/goby_round/fish_goby_round.png new file mode 100644 index 0000000..89e3f78 Binary files /dev/null and b/fish/species/goby_round/fish_goby_round.png differ diff --git a/fish/species/goby_round/fish_goby_round.png.import b/fish/species/goby_round/fish_goby_round.png.import new file mode 100644 index 0000000..6fb96e8 --- /dev/null +++ b/fish/species/goby_round/fish_goby_round.png.import @@ -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 diff --git a/fish/species/goby_round/goby_round.tres b/fish/species/goby_round/goby_round.tres new file mode 100644 index 0000000..44aa8fa --- /dev/null +++ b/fish/species/goby_round/goby_round.tres @@ -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") diff --git a/fish/species/salmon_atlantic/fish_salmon_atlantic.png b/fish/species/salmon_atlantic/fish_salmon_atlantic.png new file mode 100644 index 0000000..ea6164b Binary files /dev/null and b/fish/species/salmon_atlantic/fish_salmon_atlantic.png differ diff --git a/fish/species/salmon_atlantic/fish_salmon_atlantic.png.import b/fish/species/salmon_atlantic/fish_salmon_atlantic.png.import new file mode 100644 index 0000000..36fb242 --- /dev/null +++ b/fish/species/salmon_atlantic/fish_salmon_atlantic.png.import @@ -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 diff --git a/fish/species/salmon_atlantic/salmon_atlantic.tres b/fish/species/salmon_atlantic/salmon_atlantic.tres new file mode 100644 index 0000000..886b348 --- /dev/null +++ b/fish/species/salmon_atlantic/salmon_atlantic.tres @@ -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") diff --git a/fish/species/salmon_chum/fish_salmon_chum.png b/fish/species/salmon_chum/fish_salmon_chum.png new file mode 100644 index 0000000..920b39d Binary files /dev/null and b/fish/species/salmon_chum/fish_salmon_chum.png differ diff --git a/fish/species/salmon_chum/fish_salmon_chum.png.import b/fish/species/salmon_chum/fish_salmon_chum.png.import new file mode 100644 index 0000000..587b83b --- /dev/null +++ b/fish/species/salmon_chum/fish_salmon_chum.png.import @@ -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 diff --git a/fish/species/salmon_chum/salmon_chum.tres b/fish/species/salmon_chum/salmon_chum.tres new file mode 100644 index 0000000..2ce0f20 --- /dev/null +++ b/fish/species/salmon_chum/salmon_chum.tres @@ -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") diff --git a/fish/species/salmon_coho/fish_salmon_coho.png b/fish/species/salmon_coho/fish_salmon_coho.png new file mode 100644 index 0000000..2caf696 Binary files /dev/null and b/fish/species/salmon_coho/fish_salmon_coho.png differ diff --git a/fish/species/salmon_coho/fish_salmon_coho.png.import b/fish/species/salmon_coho/fish_salmon_coho.png.import new file mode 100644 index 0000000..1cd3254 --- /dev/null +++ b/fish/species/salmon_coho/fish_salmon_coho.png.import @@ -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 diff --git a/fish/species/salmon_coho/salmon_coho.tres b/fish/species/salmon_coho/salmon_coho.tres new file mode 100644 index 0000000..66203de --- /dev/null +++ b/fish/species/salmon_coho/salmon_coho.tres @@ -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") diff --git a/fish/species/salmon_pink/fish_salmon_pink.png b/fish/species/salmon_pink/fish_salmon_pink.png new file mode 100644 index 0000000..85b1d88 Binary files /dev/null and b/fish/species/salmon_pink/fish_salmon_pink.png differ diff --git a/fish/species/salmon_pink/fish_salmon_pink.png.import b/fish/species/salmon_pink/fish_salmon_pink.png.import new file mode 100644 index 0000000..caf4c74 --- /dev/null +++ b/fish/species/salmon_pink/fish_salmon_pink.png.import @@ -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 diff --git a/fish/species/salmon_pink/salmon_pink.tres b/fish/species/salmon_pink/salmon_pink.tres new file mode 100644 index 0000000..a6b74d5 --- /dev/null +++ b/fish/species/salmon_pink/salmon_pink.tres @@ -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") diff --git a/fish/species/salmon_sockeye/fish_salmon_sockeye.png b/fish/species/salmon_sockeye/fish_salmon_sockeye.png new file mode 100644 index 0000000..bb98433 Binary files /dev/null and b/fish/species/salmon_sockeye/fish_salmon_sockeye.png differ diff --git a/fish/species/salmon_sockeye/fish_salmon_sockeye.png.import b/fish/species/salmon_sockeye/fish_salmon_sockeye.png.import new file mode 100644 index 0000000..71a8856 --- /dev/null +++ b/fish/species/salmon_sockeye/fish_salmon_sockeye.png.import @@ -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 diff --git a/fish/species/salmon_sockeye/salmon_sockeye.tres b/fish/species/salmon_sockeye/salmon_sockeye.tres new file mode 100644 index 0000000..54e6bff --- /dev/null +++ b/fish/species/salmon_sockeye/salmon_sockeye.tres @@ -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") diff --git a/fish/species/tuna_albacore/fish_tuna_albacore.png b/fish/species/tuna_albacore/fish_tuna_albacore.png new file mode 100644 index 0000000..3769c08 Binary files /dev/null and b/fish/species/tuna_albacore/fish_tuna_albacore.png differ diff --git a/fish/species/tuna_albacore/fish_tuna_albacore.png.import b/fish/species/tuna_albacore/fish_tuna_albacore.png.import new file mode 100644 index 0000000..f2e6b6f --- /dev/null +++ b/fish/species/tuna_albacore/fish_tuna_albacore.png.import @@ -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 diff --git a/fish/species/tuna_albacore/tuna_albacore.tres b/fish/species/tuna_albacore/tuna_albacore.tres new file mode 100644 index 0000000..f98cd96 --- /dev/null +++ b/fish/species/tuna_albacore/tuna_albacore.tres @@ -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") diff --git a/fish/species/tuna_bigeye/fish_tuna_bigeye.png b/fish/species/tuna_bigeye/fish_tuna_bigeye.png new file mode 100644 index 0000000..b4a36c3 Binary files /dev/null and b/fish/species/tuna_bigeye/fish_tuna_bigeye.png differ diff --git a/fish/species/tuna_bigeye/fish_tuna_bigeye.png.import b/fish/species/tuna_bigeye/fish_tuna_bigeye.png.import new file mode 100644 index 0000000..3539d10 --- /dev/null +++ b/fish/species/tuna_bigeye/fish_tuna_bigeye.png.import @@ -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 diff --git a/fish/species/tuna_bigeye/tuna_bigeye.tres b/fish/species/tuna_bigeye/tuna_bigeye.tres new file mode 100644 index 0000000..618c7d3 --- /dev/null +++ b/fish/species/tuna_bigeye/tuna_bigeye.tres @@ -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") diff --git a/fish/species/tuna_bluefin/fish_tuna_bluefin.png b/fish/species/tuna_bluefin/fish_tuna_bluefin.png new file mode 100644 index 0000000..a2b3cad Binary files /dev/null and b/fish/species/tuna_bluefin/fish_tuna_bluefin.png differ diff --git a/fish/species/tuna_bluefin/fish_tuna_bluefin.png.import b/fish/species/tuna_bluefin/fish_tuna_bluefin.png.import new file mode 100644 index 0000000..674a0b7 --- /dev/null +++ b/fish/species/tuna_bluefin/fish_tuna_bluefin.png.import @@ -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 diff --git a/fish/species/tuna_bluefin/tuna_bluefin.tres b/fish/species/tuna_bluefin/tuna_bluefin.tres new file mode 100644 index 0000000..50591c3 --- /dev/null +++ b/fish/species/tuna_bluefin/tuna_bluefin.tres @@ -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") diff --git a/fish/species/tuna_skipjack/fish_tuna_skipjack.png b/fish/species/tuna_skipjack/fish_tuna_skipjack.png new file mode 100644 index 0000000..5a7f74d Binary files /dev/null and b/fish/species/tuna_skipjack/fish_tuna_skipjack.png differ diff --git a/fish/species/tuna_skipjack/fish_tuna_skipjack.png.import b/fish/species/tuna_skipjack/fish_tuna_skipjack.png.import new file mode 100644 index 0000000..786e985 --- /dev/null +++ b/fish/species/tuna_skipjack/fish_tuna_skipjack.png.import @@ -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 diff --git a/fish/species/tuna_skipjack/tuna_skipjack.tres b/fish/species/tuna_skipjack/tuna_skipjack.tres new file mode 100644 index 0000000..c1174e6 --- /dev/null +++ b/fish/species/tuna_skipjack/tuna_skipjack.tres @@ -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") diff --git a/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png b/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png new file mode 100644 index 0000000..a1afc81 Binary files /dev/null and b/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png differ diff --git a/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png.import b/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png.import new file mode 100644 index 0000000..a8918aa --- /dev/null +++ b/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png.import @@ -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 diff --git a/fish/species/tuna_yellowfin/tuna_yellowfin.tres b/fish/species/tuna_yellowfin/tuna_yellowfin.tres new file mode 100644 index 0000000..6fff5df --- /dev/null +++ b/fish/species/tuna_yellowfin/tuna_yellowfin.tres @@ -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") diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index a12427b..284b6de 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -122,6 +122,9 @@ const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1 @onready var _catch_controller: CatchControllerType = %CatchController @onready var _presentation: FishingPresentationType = %FishingPresentation +@onready var _bobber_audio: AudioStreamPlayer = %BobberAudio +@onready var _fight_audio: AudioStreamPlayer = %FightAudio +@onready var _reeling_audio: AudioStreamPlayer = %ReelingAudio var state: FishingState = FishingState.READY var _external_input_blocked: bool = false @@ -177,6 +180,12 @@ var _pending_cleanup_message: String = "" func _ready() -> void: _bite_rng.randomize() _configure_surface_resolver() + var fight_stream := _fight_audio.stream as AudioStreamWAV + if fight_stream != null: + _configure_audio_loop(fight_stream) + var reeling_stream := _reeling_audio.stream as AudioStreamWAV + if reeling_stream != null: + _configure_audio_loop(reeling_stream) _catch_controller.encounter_updated.connect(_on_catch_encounter_updated) _catch_controller.caught.connect(_on_catch_completed) _catch_controller.escaped.connect(_on_catch_escaped) @@ -223,6 +232,15 @@ func setup( _network_fishing = network_fishing _world_time = world_time _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: _network_fishing.local_cast_accepted.connect( _on_network_cast_accepted @@ -348,6 +366,7 @@ func set_local_menu_input_suppressed( _local_menu_input_owners.erase(owner) if suppressed and state == FishingState.WAITING_FOR_BITE: _withdrawal_input_held = false + _stop_reeling_audio() _presentation.set_line_mode( FishingPresentationType.LineMode.SLACK ) @@ -357,6 +376,7 @@ func set_gameplay_input_enabled(enabled: bool) -> void: _gameplay_input_enabled = enabled if not enabled: _local_menu_input_owners.clear() + _stop_reeling_audio() func configure_accessibility_auto_click( @@ -400,6 +420,8 @@ func _secure_showcase_catch_for_recovery() -> void: func _exit_tree() -> void: + _stop_fight_audio() + _stop_reeling_audio() _showcase_restore_generation += 1 if ( state == FishingState.SHOWING_CATCH @@ -531,6 +553,7 @@ func _unhandled_input(event: InputEvent) -> void: return FishingState.WAITING_FOR_BITE: _withdrawal_input_held = true + _start_reeling_audio() if _network_fishing != null: _network_primary_input_held = true _network_input_resend_elapsed = 0.0 @@ -546,6 +569,7 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() elif state == FishingState.WAITING_FOR_BITE: _withdrawal_input_held = false + _stop_reeling_audio() if _network_fishing != null: _network_primary_input_held = false _network_input_resend_elapsed = 0.0 @@ -731,6 +755,7 @@ func _on_cast_completed() -> void: if not _is_cast_target_valid(_cast_target): _cleanup_attempt("", &"invalid") return + _play_bobber_audio() if _network_fishing != null: _network_fishing.request_local_cast( _cast_origin_position, @@ -816,6 +841,7 @@ func _update_waiting_for_bite(delta: float) -> void: and not Input.is_action_pressed("fish_primary") ): _withdrawal_input_held = false + _stop_reeling_audio() _presentation.set_line_mode(FishingPresentationType.LineMode.SLACK) if not _withdrawal_input_held: @@ -908,6 +934,7 @@ func _activate_bite() -> void: if state != FishingState.WAITING_FOR_BITE: return + _stop_reeling_audio() if ( _active_player == null or _selected_fish == null @@ -923,6 +950,7 @@ func _activate_bite() -> void: if _pending_catch == null or not _pending_catch.is_valid(): _cancel_attempt() return + _start_fight_audio() bite_activated.emit() status_changed.emit("fish on!") _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(): _cancel_attempt() return + _stop_fight_audio() state = FishingState.SHOWING_CATCH _showcase_ready = false _showcase_outcome_completed = false @@ -1014,6 +1043,7 @@ func _on_catch_escaped() -> void: if state != FishingState.FIGHTING: return + _stop_fight_audio() var fish_name: String = "the fish" if _selected_fish != null and not _selected_fish.display_name.is_empty(): fish_name = "the %s" % _selected_fish.display_name @@ -1120,6 +1150,8 @@ func _cleanup_attempt( cooldown_message: String = "", visual_outcome: StringName = &"", ) -> void: + _stop_fight_audio() + _stop_reeling_audio() if not visual_outcome.is_empty(): if state == FishingState.RETURNING: return @@ -1422,12 +1454,14 @@ func _on_network_cast_rejected(message: String) -> void: func _on_network_bite_started(_attempt_id: String) -> void: if state != FishingState.WAITING_FOR_BITE: return + _stop_reeling_audio() state = FishingState.FIGHTING _withdrawal_input_held = false _network_primary_input_held = Input.is_action_pressed("fish_primary") _network_input_resend_elapsed = 0.0 _network_auto_click_accumulator = 0.0 _network_active_barrier_index = -1 + _start_fight_audio() bite_activated.emit() status_changed.emit("fish on!") _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) @@ -1510,6 +1544,7 @@ func _update_network_auto_click(delta: float) -> void: func _on_network_catch_received(fish_catch: FishCatchType) -> void: if state != FishingState.FIGHTING or fish_catch == null: return + _stop_fight_audio() _pending_catch = fish_catch state = FishingState.SHOWING_CATCH _showcase_ready = false @@ -1541,6 +1576,57 @@ func _on_network_attempt_ended( _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( from: Vector3, to: Vector3, diff --git a/fishing/fishing_spot.tscn b/fishing/fishing_spot.tscn index 322f618..09cf1ae 100644 --- a/fishing/fishing_spot.tscn +++ b/fishing/fishing_spot.tscn @@ -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="AudioStream" path="res://audio/sfx/fishing/fighting.wav" id="2_fighting_audio"] +[ext_resource type="AudioStream" path="res://audio/sfx/fishing/reeling.wav" id="2_reeling_audio"] +[ext_resource type="AudioStream" path="res://audio/sfx/fishing/bobber.wav" id="2_bobber_audio"] [ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"] [ext_resource type="Script" path="res://fishing/catch_controller.gd" id="4_catch"] @@ -41,6 +44,24 @@ size = Vector3(0.62, 0.04, 0.09) [node name="FishingSpot" type="Node3D"] script = ExtResource("1_spot") +[node name="BobberAudio" type="AudioStreamPlayer" parent="."] +unique_name_in_owner = true +stream = ExtResource("2_bobber_audio") +volume_db = -14.0 +bus = &"SFX" + +[node name="FightAudio" type="AudioStreamPlayer" parent="."] +unique_name_in_owner = true +stream = ExtResource("2_fighting_audio") +volume_db = -10.0 +bus = &"SFX" + +[node name="ReelingAudio" type="AudioStreamPlayer" parent="."] +unique_name_in_owner = true +stream = ExtResource("2_reeling_audio") +volume_db = -10.0 +bus = &"SFX" + [node name="CatchController" type="Node" parent="."] unique_name_in_owner = true script = ExtResource("4_catch") diff --git a/main/main.gd b/main/main.gd index 8b14388..059d0b1 100644 --- a/main/main.gd +++ b/main/main.gd @@ -201,9 +201,15 @@ var _application_initialized := false var _pending_existing_root_path := "" var _local_recovery_attempt_id: String = "" +@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience + func _ready() -> void: DisplayServer.window_set_title("NETfishing") + _shoreline_ambience.configure( + _player, + _test_world.get_saltwater_shoreline_mesh(), + ) var menu_pattern_material := ( _player_menu_backdrop.material as ShaderMaterial ) @@ -1026,6 +1032,7 @@ func _reset_pixelation() -> void: func _set_gameplay_active(active: bool) -> void: _gameplay_started = active + _shoreline_ambience.set_active(active) _title_background.visible = not active _world_pixelation.set_gameplay_active(active) _ui_pixelation.set_gameplay_active(active) diff --git a/main/main.tscn b/main/main.tscn index 03a38ab..8f66df1 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -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://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://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"] shader = ExtResource("40_title_water") @@ -277,6 +279,15 @@ stream = ExtResource("13_title_music") volume_db = -80.0 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")] unique_name_in_owner = true diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh new file mode 100755 index 0000000..1fa7312 --- /dev/null +++ b/scripts/run_validations.sh @@ -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' diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index 0f54ca7..51dca5d 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -48,7 +48,7 @@ func _run() -> void: as PlayerAssetReservationService ) assert(player != null) - assert(catalog != null and catalog.candidates.size() == 8) + assert(catalog != null and catalog.candidates.size() == 19) assert(sale_service != null) assert(shop_service != null) assert(session != null and session.is_host()) diff --git a/tests/fish_catalog_content_validation.gd b/tests/fish_catalog_content_validation.gd index 2a74663..59aaf3a 100644 --- a/tests/fish_catalog_content_validation.gd +++ b/tests/fish_catalog_content_validation.gd @@ -40,8 +40,22 @@ const CATFISH_IDS: Array[StringName] = [ const FRESH_WATER_IDS: Array[StringName] = [ &"bluegill", &"carp", &"catfish_blue", &"catfish_channel", &"catfish_flathead", &"catfish_white", + &"goby_round", +] +const SALT_WATER_IDS: Array[StringName] = [ + &"bass", + &"sunfish", + &"tuna_albacore", + &"tuna_bigeye", + &"tuna_bluefin", + &"tuna_skipjack", + &"tuna_yellowfin", + &"salmon_atlantic", + &"salmon_chum", + &"salmon_coho", + &"salmon_pink", + &"salmon_sockeye", ] -const SALT_WATER_IDS: Array[StringName] = [&"bass", &"sunfish"] func _initialize() -> void: @@ -58,9 +72,9 @@ func _run() -> void: func _validate_catalog_and_pools() -> void: - assert(Catalog.candidates.size() == 8) - assert(PondPool.candidates.size() == 6) - assert(OceanPool.candidates.size() == 2) + assert(Catalog.candidates.size() == 19) + assert(PondPool.candidates.size() == 7) + assert(OceanPool.candidates.size() == 12) for fish_id: StringName in ORIGINAL_IDS: var original_fish: FishDataType = Catalog.get_fish_by_id(fish_id) 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_max == int(values[5])) + var expected_tuna_values: Dictionary[StringName, Array] = { + &"tuna_albacore": [2, 1.5, 10.0, 45.0, 12, 24], + &"tuna_bigeye": [3, 0.55, 20.0, 120.0, 22, 48], + &"tuna_bluefin": [4, 0.18, 30.0, 250.0, 35, 90], + &"tuna_skipjack": [1, 3.0, 5.0, 25.0, 8, 16], + &"tuna_yellowfin": [2, 1.0, 15.0, 80.0, 16, 36], + } + for fish_id: StringName in expected_tuna_values: + var fish: FishDataType = Catalog.get_fish_by_id(fish_id) + var values: Array = expected_tuna_values[fish_id] + assert(fish != null and fish.is_selectable()) + assert(int(fish.rarity) == int(values[0])) + assert(is_equal_approx(fish.base_catch_weight, float(values[1]))) + assert(is_equal_approx(fish.weight_min_lb, float(values[2]))) + assert(is_equal_approx(fish.weight_max_lb, float(values[3]))) + assert(fish.sell_value_min == int(values[4])) + assert(fish.sell_value_max == int(values[5])) + + var expected_new_values: Dictionary[StringName, Array] = { + &"goby_round": [0, 8.0, 0.1, 0.6, 2, 3], + &"salmon_atlantic": [3, 0.65, 8.0, 40.0, 20, 42], + &"salmon_chum": [1, 2.5, 6.0, 25.0, 10, 22], + &"salmon_coho": [2, 1.4, 5.0, 30.0, 14, 28], + &"salmon_pink": [1, 3.0, 3.0, 12.0, 8, 15], + &"salmon_sockeye": [2, 1.6, 4.0, 15.0, 12, 24], + } + for fish_id: StringName in expected_new_values: + var fish: FishDataType = Catalog.get_fish_by_id(fish_id) + var values: Array = expected_new_values[fish_id] + assert(fish != null and fish.is_selectable()) + assert(int(fish.rarity) == int(values[0])) + assert(is_equal_approx(fish.base_catch_weight, float(values[1]))) + assert(is_equal_approx(fish.weight_min_lb, float(values[2]))) + assert(is_equal_approx(fish.weight_max_lb, float(values[3]))) + assert(fish.sell_value_min == int(values[4])) + assert(fish.sell_value_max == int(values[5])) + var pond_context := FishingContextType.new() pond_context.location_tags = [&"starter_pond"] pond_context.water_type = WaterType.Type.FRESH_WATER diff --git a/tests/fishing_audio_validation.gd b/tests/fishing_audio_validation.gd new file mode 100644 index 0000000..02a59eb --- /dev/null +++ b/tests/fishing_audio_validation.gd @@ -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() diff --git a/tests/fishing_audio_validation.gd.uid b/tests/fishing_audio_validation.gd.uid new file mode 100644 index 0000000..e9cd6d7 --- /dev/null +++ b/tests/fishing_audio_validation.gd.uid @@ -0,0 +1 @@ +uid://bby8mv7bsjd3w diff --git a/tests/logbook_runtime_validation.gd b/tests/logbook_runtime_validation.gd index b201b9a..923bbf1 100644 --- a/tests/logbook_runtime_validation.gd +++ b/tests/logbook_runtime_validation.gd @@ -49,20 +49,20 @@ func _run() -> void: assert(not hotbar.visible) var entry_buttons: Dictionary = logbook.get("_entry_buttons") - assert(entry_buttons.size() == 6) + assert(entry_buttons.size() == 7) await _capture_if_requested("-unknown") logbook.call( "_select_category", WaterType.Type.FRESH_WATER ) 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") logbook.call("_select_category", WaterType.Type.SALT_WATER) 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) 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") await process_frame logbook.call("_select_entry", &"bluegill", &"bluegill") @@ -101,7 +101,7 @@ func _validate_save_round_trip( var player := main.get("_player") as Player var catalog := main.get("fish_catalog") as FishPool assert(catalog != null) - assert(catalog.candidates.size() == 8) + assert(catalog.candidates.size() == 19) for index: int in 4: _add_test_catch(player, catalog.candidates[index]) 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.collection_log.replace_discovered_ids(no_discoveries)) 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: assert(player.inventory.get_count(fish.id) == 1) assert(player.collection_log.has_discovered(fish.id)) diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd index 4c25e35..8003fa4 100644 --- a/tests/logbook_validation.gd +++ b/tests/logbook_validation.gd @@ -49,6 +49,17 @@ func _validate_catalog() -> void: &"catfish_channel", &"catfish_flathead", &"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) for index: int in expected_ids.size(): @@ -62,7 +73,20 @@ func _validate_catalog() -> void: ) var expected_category: WaterType.Type = ( 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 ) assert( @@ -105,7 +129,7 @@ func _validate_page() -> void: page.call("_select_category", category) await create_timer(0.25).timeout 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( CatalogResource.candidates ): @@ -155,7 +179,7 @@ func _validate_page() -> void: candidate.display_name ) ) - assert(silhouette_count == 8) + assert(silhouette_count == 19) page.call("_select_category", WaterType.Type.OTHER) await create_timer(0.25).timeout diff --git a/tests/shoreline_ambience_validation.gd b/tests/shoreline_ambience_validation.gd new file mode 100644 index 0000000..505452a --- /dev/null +++ b/tests/shoreline_ambience_validation.gd @@ -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() diff --git a/tests/shoreline_ambience_validation.gd.uid b/tests/shoreline_ambience_validation.gd.uid new file mode 100644 index 0000000..4b7987b --- /dev/null +++ b/tests/shoreline_ambience_validation.gd.uid @@ -0,0 +1 @@ +uid://bwvhgpbkb00l1 diff --git a/tests/world_time_validation.gd b/tests/world_time_validation.gd index a5fde90..1fed6e7 100644 --- a/tests/world_time_validation.gd +++ b/tests/world_time_validation.gd @@ -17,6 +17,12 @@ const FishableWaterRegionType = preload( const WeatherIconType = preload("res://ui/weather_icon.gd") const Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres") +const SaltWaterMaterial: ShaderMaterial = preload( + "res://world/materials/stylized_water.tres" +) +const FreshWaterMaterial: ShaderMaterial = preload( + "res://world/materials/stylized_water_fresh.tres" +) func _initialize() -> void: @@ -138,7 +144,21 @@ func _validate_fishing_availability() -> void: assert(not night_fish.availability.is_available(day_context)) assert(night_fish.availability.is_available(night_context)) assert(night_fish.availability.is_available(transition_context)) - for fish_id: StringName in [&"bass", &"carp"]: + for fish_id: StringName in [ + &"bass", + &"carp", + &"tuna_albacore", + &"tuna_bigeye", + &"tuna_bluefin", + &"tuna_skipjack", + &"tuna_yellowfin", + &"goby_round", + &"salmon_atlantic", + &"salmon_chum", + &"salmon_coho", + &"salmon_pink", + &"salmon_sockeye", + ]: var all_time_fish: FishDataType = Catalog.get_fish_by_id(fish_id) assert(all_time_fish.availability.is_available(day_context)) assert(all_time_fish.availability.is_available(night_context)) @@ -229,6 +249,14 @@ func _validate_environment_presentation() -> void: < 0.01 ) 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) var night_horizon: Color = runtime_sky_material.get_shader_parameter( "sky_horizon_color" @@ -251,6 +279,47 @@ func _validate_environment_presentation() -> void: assert(night_moon_direction.y > 0.85) assert(night_moon_direction.is_equal_approx(-night_sun_direction)) assert(runtime_environment.ambient_light_energy < day_ambient_energy) + 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) var dusk_horizon: Color = runtime_sky_material.get_shader_parameter( "sky_horizon_color" diff --git a/ui/logbook_catalog.gd b/ui/logbook_catalog.gd index ad1a9c9..79c9df9 100644 --- a/ui/logbook_catalog.gd +++ b/ui/logbook_catalog.gd @@ -14,6 +14,17 @@ const CATALOG_ORDER: Array[StringName] = [ &"catfish_channel", &"catfish_flathead", &"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 @@ -52,6 +63,50 @@ const FISH_FACTS: Dictionary[StringName, String] = { "white catfish began along the atlantic coast between new york " + "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: diff --git a/world/environment/netfishing_sky.gdshader b/world/environment/netfishing_sky.gdshader index b70ab79..bff5c56 100644 --- a/world/environment/netfishing_sky.gdshader +++ b/world/environment/netfishing_sky.gdshader @@ -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 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 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 vec4 sun_color : source_color = vec4(1.0, 0.86, 0.46, 1.0); @@ -39,7 +40,11 @@ void sky() { vec3 base_color = mix( ground_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)); diff --git a/world/materials/stylized_water.gdshader b/world/materials/stylized_water.gdshader index 76bc1a7..0bc45e1 100644 --- a/world/materials/stylized_water.gdshader +++ b/world/materials/stylized_water.gdshader @@ -1,5 +1,5 @@ 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; @@ -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_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 float surface_view_depth; @@ -233,6 +235,8 @@ void fragment() { water_color = mix(water_color, deep_color.rgb, distant_mix * 0.72); water_alpha = mix(water_alpha, 0.96, distant_mix); + water_color *= environment_tint.rgb * environment_brightness; + ALBEDO = water_color; ALPHA = clamp(water_alpha, 0.1, 0.96); METALLIC = 0.0; diff --git a/world/materials/stylized_water.tres b/world/materials/stylized_water.tres index 40fc17d..100c598 100644 --- a/world/materials/stylized_water.tres +++ b/world/materials/stylized_water.tres @@ -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/distant_alpha_start = 260.0 shader_parameter/distant_alpha_end = 1400.0 +shader_parameter/environment_tint = Color(1, 1, 1, 1) +shader_parameter/environment_brightness = 1.0 diff --git a/world/materials/stylized_water_fresh.tres b/world/materials/stylized_water_fresh.tres index 66cc373..6634e2a 100644 --- a/world/materials/stylized_water_fresh.tres +++ b/world/materials/stylized_water_fresh.tres @@ -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/distant_alpha_start = 260.0 shader_parameter/distant_alpha_end = 1400.0 +shader_parameter/environment_tint = Color(1, 1, 1, 1) +shader_parameter/environment_brightness = 1.0 diff --git a/world/regions/starter_island_region.gd b/world/regions/starter_island_region.gd index 399c937..c03243f 100644 --- a/world/regions/starter_island_region.gd +++ b/world/regions/starter_island_region.gd @@ -58,6 +58,10 @@ func get_pelican_landmark() -> 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: var collision_shape: CollisionShape3D = ( get_node_or_null(terrain_collision_shape_path) as CollisionShape3D diff --git a/world/shoreline_ambience.gd b/world/shoreline_ambience.gd new file mode 100644 index 0000000..959cb93 --- /dev/null +++ b/world/shoreline_ambience.gd @@ -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)) diff --git a/world/shoreline_ambience.gd.uid b/world/shoreline_ambience.gd.uid new file mode 100644 index 0000000..3188ec7 --- /dev/null +++ b/world/shoreline_ambience.gd.uid @@ -0,0 +1 @@ +uid://b0c3o2vy76fg3 diff --git a/world/test_world.gd b/world/test_world.gd index 189c119..5d6660a 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -61,6 +61,10 @@ func get_pelican_convenience_landmark() -> Node3D: return _starter_island.get_pelican_landmark() +func get_saltwater_shoreline_mesh() -> MeshInstance3D: + return _starter_island.get_saltwater_shoreline_mesh() + + func _get_regions() -> Array[WorldRegion]: var regions: Array[WorldRegion] = [] for child: Node in _regions_root.get_children(): diff --git a/world/world_time_visual_controller.gd b/world/world_time_visual_controller.gd index b7b1839..c6a9ed4 100644 --- a/world/world_time_visual_controller.gd +++ b/world/world_time_visual_controller.gd @@ -9,6 +9,12 @@ const RAIN_PARTICLE_AMOUNT: int = 560 const RAIN_VELOCITY_MIN: float = 16.0 const RAIN_VELOCITY_MAX: float = 20.0 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_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_FOG := Color(0.13, 0.21, 0.32) 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_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_FOG := Color(0.74, 0.39, 0.27) 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 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 _weather_service: WorldWeatherService var _world_environment: WorldEnvironment @@ -216,38 +220,31 @@ func _apply_time(time_hours: float) -> void: var fog_color: Color = _blended_color( NIGHT_FOG, DAY_FOG, WARM_FOG, daylight, warmth ) - var weather_tint: Color = _weather_color() - var tint_strength: float = _weather_value( - 0.0, 0.30, 0.48, 0.62 + _sky_material.set_shader_parameter( + "sky_top_color", sky_top ) _sky_material.set_shader_parameter( - "sky_top_color", sky_top.lerp(weather_tint, tint_strength) - ) - _sky_material.set_shader_parameter( - "sky_horizon_color", sky_horizon.lerp(weather_tint, tint_strength) + "sky_horizon_color", + sky_horizon, ) _sky_material.set_shader_parameter( "ground_bottom_color", - ground_bottom.lerp(weather_tint, tint_strength * 0.62), + ground_bottom, ) _sky_material.set_shader_parameter( "ground_horizon_color", - ground_horizon.lerp(weather_tint, tint_strength * 0.76), + ground_horizon, ) _environment.background_energy_multiplier = ( lerpf(0.52, 0.85, daylight) - * _weather_value(1.0, 0.82, 0.66, 0.74) - ) - _environment.ambient_light_color = ambient.lerp( - weather_tint, tint_strength * 0.45 + * _weather_value(1.0, 0.82, 0.66, 1.0) ) + _environment.ambient_light_color = ambient _environment.ambient_light_energy = ( lerpf(0.66, 1.08, daylight) * _weather_value(1.0, 0.88, 0.76, 0.82) ) - _environment.fog_light_color = fog_color.lerp( - weather_tint, tint_strength * 0.82 - ) + _environment.fog_light_color = fog_color _environment.fog_light_energy = ( lerpf(0.56, 0.82, daylight) * _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( 0.35, 0.48, 0.62, 0.92 ) - _environment.fog_sky_affect = _weather_value( - 0.35, 0.48, 0.68, 0.94 - ) + _environment.fog_sky_affect = 0.0 _environment.fog_depth_curve = _weather_value( 1.6, 1.5, 1.4, 1.18 ) _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( - 170.0, 140.0, 95.0, 42.0 + 170.0, 140.0, 95.0, 28.0 ) + _apply_water_environment(daylight, warmth) _environment.adjustment_brightness = ( 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 = ( 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( -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( "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( "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_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( NIGHT_SUN, DAY_SUN, WARM_SUN, daylight, warmth @@ -308,6 +304,32 @@ func _apply_time(time_hours: float) -> void: _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( weather: WorldWeatherService.Weather, _seconds_remaining: float, @@ -349,25 +371,6 @@ func _weather_state_value( 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: if _rain == null: return