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/art/exported/environment/textures/grass_lite.png.import b/art/exported/environment/textures/grass_lite.png.import index a5ec41c..a878153 100644 --- a/art/exported/environment/textures/grass_lite.png.import +++ b/art/exported/environment/textures/grass_lite.png.import @@ -4,15 +4,16 @@ importer="texture" type="CompressedTexture2D" uid="uid://crci2on30ctoc" path.s3tc="res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.s3tc.ctex" +path.etc2="res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.etc2.ctex" metadata={ -"imported_formats": ["s3tc_bptc"], +"imported_formats": ["s3tc_bptc", "etc2_astc"], "vram_texture": true } [deps] source_file="res://art/exported/environment/textures/grass_lite.png" -dest_files=["res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.s3tc.ctex"] +dest_files=["res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.s3tc.ctex", "res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.etc2.ctex"] [params] diff --git a/art/exported/environment/textures/grass_olive_tile.png.import b/art/exported/environment/textures/grass_olive_tile.png.import index bd6ee69..c880374 100644 --- a/art/exported/environment/textures/grass_olive_tile.png.import +++ b/art/exported/environment/textures/grass_olive_tile.png.import @@ -4,15 +4,16 @@ importer="texture" type="CompressedTexture2D" uid="uid://btxhq3rdlum2" path.s3tc="res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.s3tc.ctex" +path.etc2="res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.etc2.ctex" metadata={ -"imported_formats": ["s3tc_bptc"], +"imported_formats": ["s3tc_bptc", "etc2_astc"], "vram_texture": true } [deps] source_file="res://art/exported/environment/textures/grass_olive_tile.png" -dest_files=["res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.s3tc.ctex"] +dest_files=["res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.s3tc.ctex", "res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.etc2.ctex"] [params] diff --git a/art/exported/environment/textures/sand.png.import b/art/exported/environment/textures/sand.png.import index 82f5791..fdf8720 100644 --- a/art/exported/environment/textures/sand.png.import +++ b/art/exported/environment/textures/sand.png.import @@ -4,15 +4,16 @@ importer="texture" type="CompressedTexture2D" uid="uid://bnje415gq85nn" path.s3tc="res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.s3tc.ctex" +path.etc2="res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.etc2.ctex" metadata={ -"imported_formats": ["s3tc_bptc"], +"imported_formats": ["s3tc_bptc", "etc2_astc"], "vram_texture": true } [deps] source_file="res://art/exported/environment/textures/sand.png" -dest_files=["res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.s3tc.ctex"] +dest_files=["res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.s3tc.ctex", "res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.etc2.ctex"] [params] 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/player/player.gd b/player/player.gd index 08c4217..99a8e65 100644 --- a/player/player.gd +++ b/player/player.gd @@ -94,6 +94,8 @@ class ShowcaseCameraSnapshot: @export var maximum_zoom: float = 8.0 @export var zoom_step: float = 0.75 @export var zoom_smoothing: float = 12.0 +@export_range(0.1, 12.0, 0.1) var controller_zoom_speed: float = 4.0 +@export_range(0.0, 1.0, 0.01) var controller_trigger_threshold: float = 0.55 @onready var _visuals: Node3D = %Visuals @onready var _character_animation_player: AnimationPlayer = ( @@ -290,16 +292,26 @@ func _process(delta: float) -> void: Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y) ) if stick.length() > controller_camera_deadzone: - var adjusted_strength: float = ( - (stick.length() - controller_camera_deadzone) - / (1.0 - controller_camera_deadzone) - ) - _rotate_camera( - stick.normalized() - * adjusted_strength - * controller_camera_speed - * delta - ) + if ( + Input.get_joy_axis(0, JOY_AXIS_TRIGGER_RIGHT) + >= controller_trigger_threshold + ): + var vertical_zoom_input: float = _apply_axis_deadzone(stick.y) + _set_target_zoom( + _target_zoom + + vertical_zoom_input * controller_zoom_speed * delta + ) + else: + var adjusted_strength: float = ( + (stick.length() - controller_camera_deadzone) + / (1.0 - controller_camera_deadzone) + ) + _rotate_camera( + stick.normalized() + * adjusted_strength + * controller_camera_speed + * delta + ) var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta) _spring_arm.spring_length = lerpf( @@ -435,6 +447,17 @@ func _set_target_zoom(value: float) -> void: _target_zoom = clampf(value, minimum_zoom, maximum_zoom) +func _apply_axis_deadzone(value: float) -> float: + var magnitude: float = absf(value) + if magnitude <= controller_camera_deadzone: + return 0.0 + return ( + signf(value) + * (magnitude - controller_camera_deadzone) + / (1.0 - controller_camera_deadzone) + ) + + func set_local_control(enabled: bool) -> void: local_control_enabled = enabled if is_node_ready(): diff --git a/project.godot b/project.godot index 9c0fd9a..9ec4067 100644 --- a/project.godot +++ b/project.godot @@ -80,13 +80,13 @@ slow_walk={ interact={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) -, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null) ] } fish_primary={ "deadzone": 0.2, "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) -, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":5,"axis_value":1.0,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":10,"pressure":0.0,"pressed":false,"script":null) ] } camera_drag={ @@ -97,13 +97,11 @@ camera_drag={ camera_zoom_in={ "deadzone": 0.2, "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":4,"canceled":false,"pressed":false,"double_click":false,"script":null) -, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":10,"pressure":0.0,"pressed":false,"script":null) ] } camera_zoom_out={ "deadzone": 0.2, "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":5,"canceled":false,"pressed":false,"double_click":false,"script":null) -, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null) ] } hotbar_1={ @@ -178,6 +176,16 @@ open_emotes={ , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null) ] } +open_quick_actions={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null) +] +} +open_chat={ +"deadzone": 0.2, +"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null) +] +} open_tacklebox={ "deadzone": 0.2, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":66,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) @@ -213,6 +221,20 @@ alternate_fishing={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":92,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +ui_accept={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null) +] +} +ui_cancel={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null) +] +} [physics] 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/android_readiness_validation.gd b/tests/android_readiness_validation.gd index 5a03dc5..480fcce 100644 --- a/tests/android_readiness_validation.gd +++ b/tests/android_readiness_validation.gd @@ -15,16 +15,30 @@ func _init() -> void: assert(_has_joypad_motion(&"move_left", JOY_AXIS_LEFT_X, -1.0)) assert(_has_joypad_motion(&"move_right", JOY_AXIS_LEFT_X, 1.0)) assert(_has_joypad_button(&"jump", JOY_BUTTON_A)) - assert(_has_joypad_button(&"interact", JOY_BUTTON_X)) - assert(_has_joypad_motion(&"fish_primary", JOY_AXIS_TRIGGER_RIGHT, 1.0)) + assert(_has_joypad_button(&"ui_accept", JOY_BUTTON_A)) + assert(_has_joypad_button(&"ui_cancel", JOY_BUTTON_B)) + assert(_has_joypad_button(&"interact", JOY_BUTTON_Y)) + assert(_has_joypad_button(&"fish_primary", JOY_BUTTON_RIGHT_SHOULDER)) + assert(not _has_joypad_motion( + &"fish_primary", JOY_AXIS_TRIGGER_LEFT, 1.0 + )) + assert(not _has_joypad_motion( + &"fish_primary", JOY_AXIS_TRIGGER_RIGHT, 1.0 + )) assert(_has_joypad_button(&"sprint", JOY_BUTTON_LEFT_STICK)) - assert(_has_joypad_button(&"camera_zoom_out", JOY_BUTTON_LEFT_SHOULDER)) - assert(_has_joypad_button(&"camera_zoom_in", JOY_BUTTON_RIGHT_SHOULDER)) + assert(not _has_joypad_button( + &"camera_zoom_out", JOY_BUTTON_LEFT_SHOULDER + )) + assert(not _has_joypad_button( + &"camera_zoom_in", JOY_BUTTON_RIGHT_SHOULDER + )) assert(_has_joypad_button(&"hotbar_previous", JOY_BUTTON_DPAD_LEFT)) assert(_has_joypad_button(&"hotbar_next", JOY_BUTTON_DPAD_RIGHT)) assert(_has_joypad_button(&"open_backpack", JOY_BUTTON_BACK)) assert(_has_joypad_button(&"open_system_menu", JOY_BUTTON_START)) assert(_has_joypad_button(&"open_emotes", JOY_BUTTON_DPAD_UP)) + assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN)) + assert(_has_joypad_button(&"open_chat", JOY_BUTTON_LEFT_SHOULDER)) print("android readiness validation passed") quit() 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/tests/world_weather_validation.gd b/tests/world_weather_validation.gd index a5838f8..1035a09 100644 --- a/tests/world_weather_validation.gd +++ b/tests/world_weather_validation.gd @@ -217,15 +217,19 @@ func _validate_weather_presentation() -> void: visuals.setup( clock, world_environment, sun, weather, rain_target ) + var runtime_environment: Environment = world_environment.environment + var clear_saturation: float = runtime_environment.adjustment_saturation visuals.apply_weather_immediately( WorldWeatherServiceType.Weather.FOGGY ) - var runtime_environment: Environment = world_environment.environment assert(runtime_environment.fog_depth_begin <= 4.01) assert(runtime_environment.fog_depth_end <= 42.01) - assert(runtime_environment.fog_sky_affect >= 0.93) + assert(is_zero_approx(runtime_environment.fog_sky_affect)) assert(runtime_environment.fog_aerial_perspective >= 0.91) - assert(runtime_environment.adjustment_saturation < 0.70) + assert(is_equal_approx( + runtime_environment.adjustment_saturation, + clear_saturation, + )) visuals.apply_weather_immediately( WorldWeatherServiceType.Weather.RAINY ) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index 9167a18..58b7c81 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -194,6 +194,13 @@ func close_chat() -> void: func _unhandled_input(event: InputEvent) -> void: + if event.is_action_pressed("open_chat"): + if _opened: + close_chat() + elif _available: + open_chat() + get_viewport().set_input_as_handled() + return if event is InputEventKey and event.pressed and not event.echo: if event.keycode in [KEY_ENTER, KEY_KP_ENTER]: if _opened: diff --git a/ui/controller_virtual_cursor.gd b/ui/controller_virtual_cursor.gd new file mode 100644 index 0000000..f56bb3e --- /dev/null +++ b/ui/controller_virtual_cursor.gd @@ -0,0 +1,40 @@ +class_name ControllerVirtualCursor +extends Control + +const CURSOR_SIZE: Vector2 = Vector2(28.0, 28.0) +const OUTER_COLOR: Color = Color(0.02, 0.10, 0.14, 0.96) +const INNER_COLOR: Color = Color(0.34, 0.86, 0.91, 1.0) +const CENTER_COLOR: Color = Color(0.96, 1.0, 1.0, 1.0) + + +func _ready() -> void: + size = CURSOR_SIZE + mouse_filter = Control.MOUSE_FILTER_IGNORE + focus_mode = Control.FOCUS_NONE + visible = false + queue_redraw() + + +func set_pointer_position(pointer_position: Vector2) -> void: + position = pointer_position - CURSOR_SIZE * 0.5 + + +func _draw() -> void: + var center: Vector2 = CURSOR_SIZE * 0.5 + draw_circle(center, 8.0, OUTER_COLOR) + draw_circle(center, 5.0, INNER_COLOR) + draw_circle(center, 1.75, CENTER_COLOR) + draw_line(Vector2(center.x, 0.0), Vector2(center.x, 6.0), CENTER_COLOR, 2.0) + draw_line( + Vector2(center.x, CURSOR_SIZE.y - 6.0), + Vector2(center.x, CURSOR_SIZE.y), + CENTER_COLOR, + 2.0, + ) + draw_line(Vector2(0.0, center.y), Vector2(6.0, center.y), CENTER_COLOR, 2.0) + draw_line( + Vector2(CURSOR_SIZE.x - 6.0, center.y), + Vector2(CURSOR_SIZE.x, center.y), + CENTER_COLOR, + 2.0, + ) diff --git a/ui/controller_virtual_cursor.gd.uid b/ui/controller_virtual_cursor.gd.uid new file mode 100644 index 0000000..309a5c4 --- /dev/null +++ b/ui/controller_virtual_cursor.gd.uid @@ -0,0 +1 @@ +uid://dehegl7ccn057 diff --git a/ui/emote_radial_menu.gd b/ui/emote_radial_menu.gd index 3ff0622..e0c35de 100644 --- a/ui/emote_radial_menu.gd +++ b/ui/emote_radial_menu.gd @@ -13,10 +13,12 @@ const SECTOR_COUNT: int = 8 const SIT_SECTOR: int = 0 const RING_RADIUS: float = 172.0 const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0) +const CONTROLLER_SELECTION_DEADZONE: float = 0.35 var _buttons: Array[BubbleButton] = [] var _is_open: bool = false var _selected_sector: int = SIT_SECTOR +var _controller_selection_mode: bool = false func _ready() -> void: @@ -42,7 +44,7 @@ func handle_input(event: InputEvent, can_open: bool) -> bool: if event.is_pressed(): if _is_open or not can_open: return _is_open - open_menu() + open_menu(event is InputEventJoypadButton) return true if not _is_open: return false @@ -53,9 +55,10 @@ func handle_input(event: InputEvent, can_open: bool) -> bool: return true -func open_menu() -> void: +func open_menu(controller_selection: bool = false) -> void: _is_open = true _selected_sector = SIT_SECTOR + _controller_selection_mode = controller_selection visible = true _layout_bubbles() _apply_selection_styles() @@ -74,7 +77,7 @@ func _process(_delta: float) -> void: if not _is_open: return _layout_bubbles() - _update_mouse_selection() + _update_selection() func _layout_bubbles() -> void: @@ -88,10 +91,27 @@ func _layout_bubbles() -> void: bubble.pivot_offset = BUBBLE_SIZE * 0.5 +func _update_selection() -> void: + var stick: Vector2 = Vector2( + Input.get_joy_axis(0, JOY_AXIS_RIGHT_X), + Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y), + ) + if stick.length() >= CONTROLLER_SELECTION_DEADZONE: + _select_sector_from_offset(stick) + return + if _controller_selection_mode: + return + _update_mouse_selection() + + func _update_mouse_selection() -> void: var offset: Vector2 = get_local_mouse_position() - size * 0.5 if offset.length() < 24.0: return + _select_sector_from_offset(offset) + + +func _select_sector_from_offset(offset: Vector2) -> void: var angle: float = fposmod(offset.angle() + PI * 0.5 + PI / 8.0, TAU) var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT)))) if sector == _selected_sector: diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 16ad69d..8b2b02d 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -34,6 +34,10 @@ const PlayerCoolerCapacityType = preload( ) const ChatUIType = preload("res://ui/chat_ui.gd") const EmoteRadialMenuType = preload("res://ui/emote_radial_menu.gd") +const QuickRadialMenuType = preload("res://ui/quick_radial_menu.gd") +const ControllerVirtualCursorType = preload( + "res://ui/controller_virtual_cursor.gd" +) const SurfaceDrawingToolbarType = preload( "res://ui/surface_drawing_toolbar.gd" ) @@ -58,6 +62,11 @@ signal passive_pointer_ui_changed(is_enabled: bool) signal player_menu_backdrop_visibility_changed(is_visible: bool) signal shop_backdrop_visibility_changed(is_visible: bool) +const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse" +const VIRTUAL_MOUSE_TRIGGER_THRESHOLD: float = 0.55 +const VIRTUAL_MOUSE_STICK_DEADZONE: float = 0.18 +const VIRTUAL_MOUSE_SPEED: float = 720.0 + @onready var _status_label: Label = %StatusLabel @onready var _gameplay_transient_hud: Control = %GameplayTransientHUD @onready var _experience_presentation: Control = %ExperiencePresentation @@ -77,6 +86,7 @@ signal shop_backdrop_visibility_changed(is_visible: bool) @onready var _experience_bubble: PanelContainer = %ExperienceBubble @onready var _experience_bubble_label: Label = %ExperienceBubbleLabel @onready var _canonical_stage: Control = %CanonicalStage +@onready var _ui_root: Control = %UIRoot @onready var _player_menu: PlayerMenuType = %PlayerMenu @onready var _screen_fade: ScreenFade = %ScreenFade @onready var _title_screen: TitleScreenType = %TitleScreen @@ -87,6 +97,10 @@ signal shop_backdrop_visibility_changed(is_visible: bool) @onready var _effect_status: Label = %EffectStatus @onready var _chat_ui: ChatUIType = %ChatUI @onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu +@onready var _quick_radial_menu: QuickRadialMenuType = %QuickRadialMenu +@onready var _controller_virtual_cursor: ControllerVirtualCursorType = ( + %ControllerVirtualCursor +) @onready var _surface_drawing_toolbar: SurfaceDrawingToolbarType = ( %SurfaceDrawingToolbar ) @@ -115,6 +129,15 @@ var _experience_award_queue: Array[Dictionary] = [] var _experience_animation_active: bool = false var _experience_animation_generation: int = 0 var _experience_panel_rest_y: float = 18.0 +var _emote_prior_camera_input_enabled: bool = true +var _quick_prior_camera_input_enabled: bool = true +var _virtual_mouse_active: bool = false +var _virtual_mouse_prior_camera_input_enabled: bool = true +var _virtual_mouse_prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE +var _virtual_mouse_window_position: Vector2 = Vector2.ZERO +var _virtual_mouse_button_mask: int = 0 +var _virtual_mouse_right_pressed: bool = false +var _injecting_virtual_mouse_event: bool = false func _ready() -> void: @@ -128,6 +151,7 @@ func _ready() -> void: presentation_parent.get_child_count() - 1, ) _emote_radial_menu.emote_selected.connect(_on_emote_selected) + _quick_radial_menu.action_selected.connect(_on_quick_action_selected) _chat_ui.text_entry_ownership_changed.connect( _on_chat_text_entry_ownership_changed ) @@ -274,6 +298,17 @@ func setup( func _input(event: InputEvent) -> void: + if ( + _virtual_mouse_active + and _injecting_virtual_mouse_event + and event is InputEventMouseMotion + ): + _update_virtual_cursor_position( + (event as InputEventMouseMotion).position + ) + if _handle_virtual_mouse_input(event): + get_viewport().set_input_as_handled() + return if ( _surface_drawing_toolbar != null and _surface_drawing_toolbar.owns_pointer_event(event) @@ -295,7 +330,7 @@ func _input(event: InputEvent) -> void: ): get_viewport().set_input_as_handled() return - if _emote_radial_menu == null: + if _emote_radial_menu == null or _quick_radial_menu == null: return var can_open: bool = ( _gameplay_ui_enabled @@ -304,8 +339,42 @@ func _input(event: InputEvent) -> void: and not _shop_open and not _chat_input_open and not _showcase_active + and not _virtual_mouse_active ) - if _emote_radial_menu.handle_input(event, can_open): + var emote_was_open: bool = _emote_radial_menu.is_open() + if _emote_radial_menu.handle_input( + event, + can_open and not _quick_radial_menu.is_open(), + ): + var emote_is_open: bool = _emote_radial_menu.is_open() + if emote_is_open != emote_was_open and _player != null: + if emote_is_open: + _emote_prior_camera_input_enabled = ( + _player.is_camera_input_enabled() + ) + _player.set_camera_input_enabled(false) + else: + _player.set_camera_input_enabled( + _emote_prior_camera_input_enabled + ) + get_viewport().set_input_as_handled() + return + var quick_was_open: bool = _quick_radial_menu.is_open() + if _quick_radial_menu.handle_input( + event, + can_open and not _emote_radial_menu.is_open(), + ): + var quick_is_open: bool = _quick_radial_menu.is_open() + if quick_is_open != quick_was_open and _player != null: + if quick_is_open: + _quick_prior_camera_input_enabled = ( + _player.is_camera_input_enabled() + ) + _player.set_camera_input_enabled(false) + else: + _player.set_camera_input_enabled( + _quick_prior_camera_input_enabled + ) get_viewport().set_input_as_handled() @@ -314,6 +383,212 @@ func _on_emote_selected(emote_id: StringName) -> void: _player.toggle_sitting() +func _on_quick_action_selected(action_id: StringName) -> void: + match action_id: + &"stuff": + _player_menu.open_section(PlayerMenuType.Section.COOLER) + &"logbook": + _player_menu.open_section(PlayerMenuType.Section.LOGBOOK) + &"fishnet": + _player_menu.open_section(PlayerMenuType.Section.NET) + &"mail": + _player_menu.open_section(PlayerMenuType.Section.MAIL) + &"profile": + _player_menu.open_section(PlayerMenuType.Section.PROFILE) + &"online": + _player_menu.open_section(PlayerMenuType.Section.PLAYERS) + &"paint": + _toggle_surface_drawing() + &"chat": + _chat_ui.open_chat() + + +func _handle_virtual_mouse_input(event: InputEvent) -> bool: + var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion + if motion_event != null: + if motion_event.axis == JOY_AXIS_TRIGGER_LEFT: + var was_active: bool = _virtual_mouse_active + if motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD: + if not _virtual_mouse_active and _can_start_virtual_mouse(): + _begin_virtual_mouse() + elif _virtual_mouse_active: + _end_virtual_mouse() + return was_active or _virtual_mouse_active + if ( + _virtual_mouse_active + and motion_event.axis == JOY_AXIS_TRIGGER_RIGHT + ): + _set_virtual_mouse_button( + MOUSE_BUTTON_RIGHT, + motion_event.axis_value >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD, + ) + return true + var button_event: InputEventJoypadButton = event as InputEventJoypadButton + if ( + button_event != null + and button_event.button_index == JOY_BUTTON_RIGHT_SHOULDER + and ( + _virtual_mouse_active + or Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT) + >= VIRTUAL_MOUSE_TRIGGER_THRESHOLD + ) + ): + if not _virtual_mouse_active and _can_start_virtual_mouse(): + _begin_virtual_mouse() + if _virtual_mouse_active: + _set_virtual_mouse_button(MOUSE_BUTTON_LEFT, button_event.pressed) + return true + return false + + +func _can_start_virtual_mouse() -> bool: + return ( + _gameplay_ui_enabled + and not _showcase_active + and _player != null + and _fishing_spot != null + and _fishing_spot.can_use_surface_drawing() + and not _emote_radial_menu.is_open() + and not _quick_radial_menu.is_open() + ) + + +func _begin_virtual_mouse() -> void: + if _virtual_mouse_active: + return + _virtual_mouse_active = true + _virtual_mouse_prior_camera_input_enabled = ( + _player.is_camera_input_enabled() + ) + _player.set_camera_input_enabled(false) + if _fishing_spot != null: + _fishing_spot.set_local_menu_input_suppressed( + VIRTUAL_MOUSE_INPUT_OWNER, + true, + ) + _virtual_mouse_prior_mouse_mode = Input.mouse_mode + Input.mouse_mode = Input.MOUSE_MODE_HIDDEN + _virtual_mouse_button_mask = 0 + _virtual_mouse_right_pressed = false + _virtual_mouse_window_position = Vector2(get_window().size) * 0.5 + _controller_virtual_cursor.visible = true + _emit_virtual_mouse_motion(Vector2.ZERO) + + +func _end_virtual_mouse() -> void: + if not _virtual_mouse_active: + return + if (_virtual_mouse_button_mask & MOUSE_BUTTON_MASK_LEFT) != 0: + _set_virtual_mouse_button(MOUSE_BUTTON_LEFT, false) + if _virtual_mouse_right_pressed: + _set_virtual_mouse_button(MOUSE_BUTTON_RIGHT, false) + _virtual_mouse_active = false + _controller_virtual_cursor.visible = false + if _fishing_spot != null: + _fishing_spot.set_local_menu_input_suppressed( + VIRTUAL_MOUSE_INPUT_OWNER, + false, + ) + if _player != null and is_instance_valid(_player): + _player.set_camera_input_enabled( + _virtual_mouse_prior_camera_input_enabled + ) + Input.mouse_mode = _virtual_mouse_prior_mouse_mode + + +func _update_virtual_mouse(delta: float) -> void: + if not _virtual_mouse_active: + return + if ( + Input.get_joy_axis(0, JOY_AXIS_TRIGGER_LEFT) + < VIRTUAL_MOUSE_TRIGGER_THRESHOLD + ): + _end_virtual_mouse() + return + var stick: Vector2 = Vector2( + Input.get_joy_axis(0, JOY_AXIS_RIGHT_X), + Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y), + ) + var stick_length: float = stick.length() + if stick_length <= VIRTUAL_MOUSE_STICK_DEADZONE: + return + var adjusted_strength: float = ( + (stick_length - VIRTUAL_MOUSE_STICK_DEADZONE) + / (1.0 - VIRTUAL_MOUSE_STICK_DEADZONE) + ) + var display_scale: float = UIReferencePresentationType.get_scale( + Vector2(get_window().size) + ) + var relative_motion: Vector2 = ( + stick.normalized() + * adjusted_strength + * VIRTUAL_MOUSE_SPEED + * display_scale + * delta + ) + var window_size: Vector2 = Vector2(get_window().size) + _virtual_mouse_window_position = Vector2( + clampf( + _virtual_mouse_window_position.x + relative_motion.x, + 0.0, + window_size.x, + ), + clampf( + _virtual_mouse_window_position.y + relative_motion.y, + 0.0, + window_size.y, + ), + ) + _emit_virtual_mouse_motion(relative_motion) + + +func _set_virtual_mouse_button(button: MouseButton, pressed: bool) -> void: + var mask: int = ( + MOUSE_BUTTON_MASK_LEFT + if button == MOUSE_BUTTON_LEFT + else MOUSE_BUTTON_MASK_RIGHT + ) + var already_pressed: bool = bool(_virtual_mouse_button_mask & mask) + if already_pressed == pressed: + return + if pressed: + _virtual_mouse_button_mask |= mask + else: + _virtual_mouse_button_mask &= ~mask + if button == MOUSE_BUTTON_RIGHT: + _virtual_mouse_right_pressed = pressed + var mouse_event := InputEventMouseButton.new() + mouse_event.position = _virtual_mouse_window_position + mouse_event.global_position = _virtual_mouse_window_position + mouse_event.button_index = button + mouse_event.button_mask = _virtual_mouse_button_mask + mouse_event.pressed = pressed + mouse_event.factor = 1.0 + _parse_virtual_mouse_event(mouse_event) + + +func _emit_virtual_mouse_motion(relative_motion: Vector2) -> void: + var motion_event := InputEventMouseMotion.new() + motion_event.position = _virtual_mouse_window_position + motion_event.global_position = _virtual_mouse_window_position + motion_event.relative = relative_motion + motion_event.button_mask = _virtual_mouse_button_mask + _parse_virtual_mouse_event(motion_event) + + +func _parse_virtual_mouse_event(event: InputEventMouse) -> void: + _injecting_virtual_mouse_event = true + Input.parse_input_event(event) + _injecting_virtual_mouse_event = false + + +func _update_virtual_cursor_position(viewport_position: Vector2) -> void: + var root_transform: Transform2D = _ui_root.get_global_transform_with_canvas() + _controller_virtual_cursor.set_pointer_position( + root_transform.affine_inverse() * viewport_position + ) + + func _toggle_surface_drawing() -> void: if _surface_drawing == null: return @@ -344,7 +619,8 @@ func setup_data_and_identity( ) -func _process(_delta: float) -> void: +func _process(delta: float) -> void: + _update_virtual_mouse(delta) _update_experience_bubble_position() if _item_effects == null or not _gameplay_ui_enabled: _effect_status.hide() @@ -403,6 +679,19 @@ func set_gameplay_ui_enabled(enabled: bool) -> void: _experience_presentation.visible = enabled _refresh_chat_availability() if not enabled: + _end_virtual_mouse() + if _emote_radial_menu != null and _emote_radial_menu.is_open(): + _emote_radial_menu.close_menu() + if _player != null: + _player.set_camera_input_enabled( + _emote_prior_camera_input_enabled + ) + if _quick_radial_menu != null and _quick_radial_menu.is_open(): + _quick_radial_menu.close_menu() + if _player != null: + _player.set_camera_input_enabled( + _quick_prior_camera_input_enabled + ) if _surface_drawing != null: _surface_drawing.deactivate() close_player_menu_for_session_end() diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 9d63e54..0726da0 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=19 format=3] +[gd_scene load_steps=21 format=3] [ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"] [ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"] @@ -11,6 +11,8 @@ [ext_resource type="Script" path="res://ui/chat_ui.gd" id="9_chat"] [ext_resource type="PackedScene" path="res://ui/emote_radial_menu.tscn" id="10_emote"] [ext_resource type="PackedScene" path="res://ui/surface_drawing_toolbar.tscn" id="11_drawing_toolbar"] +[ext_resource type="PackedScene" path="res://ui/quick_radial_menu.tscn" id="12_quick"] +[ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"] [sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"] bg_color = Color(0.032, 0.118, 0.15, 1) @@ -328,6 +330,9 @@ unique_name_in_owner = true [node name="EmoteRadialMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("10_emote")] unique_name_in_owner = true +[node name="QuickRadialMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("12_quick")] +unique_name_in_owner = true + [node name="ShopPrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false @@ -386,6 +391,12 @@ grow_vertical = 2 mouse_filter = 2 script = ExtResource("9_chat") +[node name="ControllerVirtualCursor" type="Control" parent="UIRoot"] +unique_name_in_owner = true +z_index = 500 +mouse_filter = 2 +script = ExtResource("13_cursor") + [node name="ScreenFade" type="ColorRect" parent="UIRoot"] unique_name_in_owner = true visible = false 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/ui/player_menu.gd b/ui/player_menu.gd index af8148d..d3ccfcf 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -701,6 +701,25 @@ func open_menu() -> void: menu_visibility_changed.emit(true) +func open_section(section: Section) -> bool: + if ( + _shop_cooler_context_active + or _player == null + or _fishing_spot == null + or not _fishing_spot.can_open_player_menu() + ): + return false + if visible: + if _transitioning or _page_transitioning or _sale_confirmation.visible: + return false + if section != _current_section: + _show_section(section) + return true + _current_section = section + open_menu() + return visible + + func mount_shop_cooler( host: Control, buyer: FishBuyerProfileType, diff --git a/ui/quick_radial_menu.gd b/ui/quick_radial_menu.gd new file mode 100644 index 0000000..585bae2 --- /dev/null +++ b/ui/quick_radial_menu.gd @@ -0,0 +1,152 @@ +class_name QuickRadialMenu +extends Control + +signal action_selected(action_id: StringName) + +const BubbleButtonScene: PackedScene = preload( + "res://ui/components/bubble_menu/bubble_button.tscn" +) +const BubbleProfile: BubbleMenuProfile = preload( + "res://ui/components/bubble_menu/bubble_menu_profile.tres" +) +const SECTOR_COUNT: int = 8 +const RING_RADIUS: float = 172.0 +const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0) +const CONTROLLER_SELECTION_DEADZONE: float = 0.35 +const ACTIONS: Array[StringName] = [ + &"stuff", + &"logbook", + &"fishnet", + &"mail", + &"profile", + &"online", + &"paint", + &"chat", +] +const LABELS: Array[String] = [ + "stuff", + "logbook", + "fishnet", + "mail", + "profile", + "online", + "paint", + "chat", +] + +var _buttons: Array[BubbleButton] = [] +var _is_open: bool = false +var _selected_sector: int = 0 +var _controller_selection_mode: bool = false + + +func _ready() -> void: + visible = false + mouse_filter = Control.MOUSE_FILTER_IGNORE + for sector: int in SECTOR_COUNT: + var bubble: BubbleButton = BubbleButtonScene.instantiate() as BubbleButton + bubble.profile = BubbleProfile + bubble.focus_mode = Control.FOCUS_NONE + bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE + bubble.text = LABELS[sector] + add_child(bubble) + _buttons.append(bubble) + _apply_selection_styles() + + +func handle_input(event: InputEvent, can_open: bool) -> bool: + if not event.is_action("open_quick_actions"): + return false + var key_event: InputEventKey = event as InputEventKey + if key_event != null and key_event.echo: + return false + if event.is_pressed(): + if _is_open or not can_open: + return _is_open + open_menu(event is InputEventJoypadButton) + return true + if not _is_open: + return false + var selected: int = _selected_sector + close_menu() + call_deferred("_emit_selected_action", ACTIONS[selected]) + return true + + +func open_menu(controller_selection: bool = false) -> void: + _is_open = true + _selected_sector = 0 + _controller_selection_mode = controller_selection + visible = true + _layout_bubbles() + _apply_selection_styles() + + +func close_menu() -> void: + _is_open = false + visible = false + + +func is_open() -> bool: + return _is_open + + +func _emit_selected_action(action_id: StringName) -> void: + action_selected.emit(action_id) + + +func _process(_delta: float) -> void: + if not _is_open: + return + _layout_bubbles() + _update_selection() + + +func _layout_bubbles() -> void: + var center: Vector2 = size * 0.5 + for sector: int in SECTOR_COUNT: + var angle: float = -PI * 0.5 + TAU * float(sector) / float(SECTOR_COUNT) + var bubble_center: Vector2 = center + Vector2.from_angle(angle) * RING_RADIUS + var bubble: BubbleButton = _buttons[sector] + bubble.position = bubble_center - BUBBLE_SIZE * 0.5 + bubble.size = BUBBLE_SIZE + bubble.pivot_offset = BUBBLE_SIZE * 0.5 + + +func _update_selection() -> void: + var stick: Vector2 = Vector2( + Input.get_joy_axis(0, JOY_AXIS_RIGHT_X), + Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y), + ) + if stick.length() >= CONTROLLER_SELECTION_DEADZONE: + _select_sector_from_offset(stick) + return + if _controller_selection_mode: + return + _update_mouse_selection() + + +func _update_mouse_selection() -> void: + var offset: Vector2 = get_local_mouse_position() - size * 0.5 + if offset.length() < 24.0: + return + _select_sector_from_offset(offset) + + +func _select_sector_from_offset(offset: Vector2) -> void: + var angle: float = fposmod(offset.angle() + PI * 0.5 + PI / 8.0, TAU) + var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT)))) + if sector == _selected_sector: + return + _selected_sector = sector + _apply_selection_styles() + + +func _apply_selection_styles() -> void: + for sector: int in _buttons.size(): + var bubble: BubbleButton = _buttons[sector] + bubble.apply_profile() + if sector == _selected_sector: + bubble.add_theme_stylebox_override( + "normal", BubbleProfile.make_hover_style() + ) diff --git a/ui/quick_radial_menu.gd.uid b/ui/quick_radial_menu.gd.uid new file mode 100644 index 0000000..ee4327f --- /dev/null +++ b/ui/quick_radial_menu.gd.uid @@ -0,0 +1 @@ +uid://5nr1c8x881sw diff --git a/ui/quick_radial_menu.tscn b/ui/quick_radial_menu.tscn new file mode 100644 index 0000000..66a0f45 --- /dev/null +++ b/ui/quick_radial_menu.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/quick_radial_menu.gd" id="1_quick"] + +[node name="QuickRadialMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +z_index = 90 +script = ExtResource("1_quick") 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