Compare commits

..

4 commits

210 changed files with 4818 additions and 1313 deletions

View file

@ -37,6 +37,8 @@ written with or without assistance.
- “Gentle Ocean Waves Loop” by kkenny101, used for the saltwater shoreline - “Gentle Ocean Waves Loop” by kkenny101, used for the saltwater shoreline
ambience. Freesound sound 852826, Creative Commons Zero (CC0): ambience. Freesound sound 852826, Creative Commons Zero (CC0):
https://freesound.org/s/852826/ https://freesound.org/s/852826/
- Rain Loop Ontario by Ayton -- https://freesound.org/s/212799/ -- License:
Attribution 3.0
- “Quick Water Droplet” by qubodup, used for the bobber water-impact sound. - “Quick Water Droplet” by qubodup, used for the bobber water-impact sound.
Freesound sound 792931, Creative Commons Zero (CC0): Freesound sound 792931, Creative Commons Zero (CC0):
https://freesound.org/s/792931/ https://freesound.org/s/792931/

View file

@ -38,32 +38,6 @@ scripts/run_validations.sh quick
Additional suites and individual commands are documented in Additional suites and individual commands are documented in
[`docs/TESTING.md`](docs/TESTING.md). [`docs/TESTING.md`](docs/TESTING.md).
## Texture import normalization
Texture import settings are cached in `.import` sidecars. To keep new textures
consistent by default, normalize imported textures before committing:
```sh
./scripts/normalize_texture_profiles.sh
```
To normalize only a specific directory (for example environment textures):
```sh
./scripts/normalize_texture_profiles.sh res://art/exported/environment/textures
```
The script enforces a single baseline texture profile by default and sets
`import_profile = "managed_default"` in `[params]`. If any texture needs
custom settings, set:
```ini
[params]
import_profile="custom"
```
in its `.import` file to opt it out.
## Building ## Building
Export presets are maintained for Linux, Windows, and Android. Repository Export presets are maintained for Linux, Windows, and Android. Repository

View file

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

View file

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

View file

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

View file

@ -1,43 +0,0 @@
[remap]
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", "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", "res://.godot/imported/grass_lite.png-f89cf6413257cd31582bd4f9f875ae85.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
import_profile="managed_default"

View file

@ -1,43 +0,0 @@
[remap]
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", "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", "res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
import_profile="managed_default"

View file

@ -1,43 +0,0 @@
[remap]
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", "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", "res://.godot/imported/sand.png-41fc96c392119deeba43c2d4e49d88a5.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
import_profile="managed_default"

View file

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

View file

@ -1,43 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ctll4fbskvat8"
path.s3tc="res://.godot/imported/sandstone_heavy.png-c324c68e8c28417ecd3506dbd5f24ef5.s3tc.ctex"
path.etc2="res://.godot/imported/sandstone_heavy.png-c324c68e8c28417ecd3506dbd5f24ef5.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://art/exported/environment/textures/sandstone_heavy.png"
dest_files=["res://.godot/imported/sandstone_heavy.png-c324c68e8c28417ecd3506dbd5f24ef5.s3tc.ctex", "res://.godot/imported/sandstone_heavy.png-c324c68e8c28417ecd3506dbd5f24ef5.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
import_profile="managed_default"

View file

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

View file

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Before After
Before After

Binary file not shown.

View file

@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://cfqskba5j2iig"
path="res://.godot/imported/rain_loop_ontario.ogg-c206e8a7ee5398c9fdc6d59462050964.oggvorbisstr"
[deps]
source_file="res://audio/ambience/rain_loop_ontario.ogg"
dest_files=["res://.godot/imported/rain_loop_ontario.ogg-c206e8a7ee5398c9fdc6d59462050964.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View file

@ -9,13 +9,33 @@ const WORM_MAX_STACK: int = 10
const ITEM_PRICES: Dictionary[StringName, int] = { const ITEM_PRICES: Dictionary[StringName, int] = {
&"worms": 1, &"worms": 1,
&"snails": 3,
&"shrimp": 7,
&"squid_chunks": 15,
&"whole_anchovy": 30,
&"whole_sardine": 60,
&"luminous_roe": 125,
&"coffee": 20, &"coffee": 20,
&"energy_drink": 35, &"energy_drink": 35,
&"snack": 30, &"snack": 30,
&"fish_finder": 60, &"fish_finder": 60,
} }
const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = {
&"snails": 400,
&"shrimp": 1200,
&"squid_chunks": 3500,
&"whole_anchovy": 9000,
&"whole_sardine": 22000,
&"luminous_roe": 50000,
}
const ITEM_ORDER: Array[StringName] = [ const ITEM_ORDER: Array[StringName] = [
&"worms", &"worms",
&"snails",
&"shrimp",
&"squid_chunks",
&"whole_anchovy",
&"whole_sardine",
&"luminous_roe",
&"coffee", &"coffee",
&"energy_drink", &"energy_drink",
&"snack", &"snack",
@ -31,14 +51,35 @@ static func get_stock_item_ids() -> Array[StringName]:
return ITEM_ORDER.duplicate() return ITEM_ORDER.duplicate()
static func get_purchase_quantity(item_id: StringName, owned: int) -> int: static func get_unlock_price(item_id: StringName) -> int:
if item_id == &"worms": return BAIT_UNLOCK_PRICES.get(item_id, -1)
return maxi(WORM_MAX_STACK - owned, 0)
static func get_purchase_quantity(
item_id: StringName,
owned: int,
maximum_stack: int = WORM_MAX_STACK,
) -> int:
if is_bait_topoff(item_id):
return maxi(maximum_stack - owned, 0)
return 1 return 1
static func is_bait_topoff(item_id: StringName) -> bool: static func is_bait_topoff(item_id: StringName) -> bool:
return item_id == &"worms" return item_id == &"worms" or BAIT_UNLOCK_PRICES.has(item_id)
static func get_purchase_cost(
item_id: StringName,
quantity: int,
bait_unlocked: bool,
) -> int:
if quantity <= 0:
return -1
if is_bait_topoff(item_id) and item_id != &"worms" and not bait_unlocked:
return get_unlock_price(item_id)
var unit_price: int = get_price(item_id)
return unit_price * quantity if unit_price >= 0 else -1
static func purchase_one( static func purchase_one(
@ -49,22 +90,32 @@ static func purchase_one(
) -> bool: ) -> bool:
if wallet == null or bag == null or catalog == null: if wallet == null or bag == null or catalog == null:
return false return false
var price: int = get_price(item_id)
var item: ItemDataType = catalog.get_item_by_id(item_id) var item: ItemDataType = catalog.get_item_by_id(item_id)
var bait_topoff: bool = is_bait_topoff(item_id)
var quantity: int = get_purchase_quantity(
item_id,
bag.get_quantity(item_id),
item.max_stack if item != null else WORM_MAX_STACK,
)
var price: int = get_purchase_cost(
item_id,
quantity,
bag.is_bait_unlocked(item_id),
)
if ( if (
price < 0 price < 0
or item == null or item == null
or not item.is_valid() or not item.is_valid()
or (item.category != ItemDataType.Category.CONSUMABLE and not is_bait_topoff(item_id)) or (item.category != ItemDataType.Category.CONSUMABLE and not bait_topoff)
or not item.stackable or not item.stackable
or (not item.usable and not is_bait_topoff(item_id)) or (not item.usable and not bait_topoff)
or not bag.can_add_item(item_id, 1) or not bag.can_add_item(item_id, quantity)
or not wallet.can_afford(price) or not wallet.can_afford(price)
): ):
return false return false
if not wallet.debit(price): if not wallet.debit(price):
return false return false
if bag.add_item(item_id, 1): if bag.add_item(item_id, quantity):
return true return true
if not wallet.credit(price): if not wallet.credit(price):
push_error("Fishing Shop failed to roll back an item purchase.") push_error("Fishing Shop failed to roll back an item purchase.")

View file

@ -16,6 +16,12 @@ const ALL_TIERS_MASK: int = (1 << TIER_COUNT) - 1
# multipliers later without changing catch serialization or tier identity. # multipliers later without changing catch serialization or tier identity.
const BASE_ROLL_WEIGHTS: Array[float] = [40.0, 32.0, 18.0, 8.0, 2.0] const BASE_ROLL_WEIGHTS: Array[float] = [40.0, 32.0, 18.0, 8.0, 2.0]
const WORM_ROLL_WEIGHTS: Array[float] = [80.0, 10.0, 5.0, 4.0, 1.0] const WORM_ROLL_WEIGHTS: Array[float] = [80.0, 10.0, 5.0, 4.0, 1.0]
const SNAIL_ROLL_WEIGHTS: Array[float] = [68.0, 16.0, 8.0, 6.0, 2.0]
const SHRIMP_ROLL_WEIGHTS: Array[float] = [56.0, 21.0, 12.0, 8.0, 3.0]
const SQUID_ROLL_WEIGHTS: Array[float] = [44.0, 25.0, 16.0, 11.0, 4.0]
const ANCHOVY_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0]
const SARDINE_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0]
const LUMINOUS_ROE_ROLL_WEIGHTS: Array[float] = [16.0, 26.0, 26.0, 20.0, 12.0]
const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0] const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0]
# Legacy profile multiplier retained for serialized/profile compatibility. # Legacy profile multiplier retained for serialized/profile compatibility.
# New encounters use the weighted quality/rarity/weight bands below. # New encounters use the weighted quality/rarity/weight bands below.
@ -136,17 +142,34 @@ static func roll(
static func roll_weights_for_bait(active_bait_tags: Array[StringName]) -> Array[float]: static func roll_weights_for_bait(active_bait_tags: Array[StringName]) -> Array[float]:
var bait_weights: Array[float] = rarity_weights_for_bait(active_bait_tags)
var weights: Array[float] = []
for quality: int in TIER_COUNT:
weights.append(bait_weights[quality] / BASE_ROLL_WEIGHTS[quality])
return weights
static func rarity_weights_for_bait(
active_bait_tags: Array[StringName],
) -> Array[float]:
var weights: Array[float] = [] var weights: Array[float] = []
if active_bait_tags.is_empty(): if active_bait_tags.is_empty():
weights.assign([2.5, 0.0, 0.0, 0.0, 0.0]) weights.assign([100.0, 0.0, 0.0, 0.0, 0.0])
elif active_bait_tags.has(&"bait_rarity_6"):
weights.assign(LUMINOUS_ROE_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_5"):
weights.assign(SARDINE_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_4"):
weights.assign(ANCHOVY_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_3"):
weights.assign(SQUID_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_2"):
weights.assign(SHRIMP_ROLL_WEIGHTS)
elif active_bait_tags.has(&"bait_rarity_1"):
weights.assign(SNAIL_ROLL_WEIGHTS)
else: else:
weights.assign([ # Unknown and legacy bait tags retain the original worms behavior.
WORM_ROLL_WEIGHTS[0] / BASE_ROLL_WEIGHTS[0], weights.assign(WORM_ROLL_WEIGHTS)
WORM_ROLL_WEIGHTS[1] / BASE_ROLL_WEIGHTS[1],
WORM_ROLL_WEIGHTS[2] / BASE_ROLL_WEIGHTS[2],
WORM_ROLL_WEIGHTS[3] / BASE_ROLL_WEIGHTS[3],
WORM_ROLL_WEIGHTS[4] / BASE_ROLL_WEIGHTS[4],
])
return weights return weights

View file

@ -81,9 +81,9 @@ func select_fish(
func _roll_rarity(context: FishingContextType) -> int: func _roll_rarity(context: FishingContextType) -> int:
var weights: Array[float] = [100.0, 0.0, 0.0, 0.0, 0.0] var weights: Array[float] = FishQualityType.rarity_weights_for_bait(
if not context.active_bait_tags.is_empty(): context.active_bait_tags
weights = [80.0, 10.0, 5.0, 4.0, 1.0] )
for index: int in range(weights.size()): for index: int in range(weights.size()):
if index < rarity_weight_multipliers.size(): if index < rarity_weight_multipliers.size():
weights[index] *= maxf(rarity_weight_multipliers[index], 0.0) weights[index] *= maxf(rarity_weight_multipliers[index], 0.0)

View file

@ -1,6 +1,10 @@
class_name FishingPresentation class_name FishingPresentation
extends Node3D extends Node3D
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
signal cast_completed signal cast_completed
signal outcome_completed(outcome: StringName) signal outcome_completed(outcome: StringName)
signal presentation_interrupted signal presentation_interrupted
@ -52,6 +56,7 @@ var _rod: Node3D
var _rod_tip: Marker3D var _rod_tip: Marker3D
var _rod_neutral_rotation: Vector3 var _rod_neutral_rotation: Vector3
var _cast_arrival_position: Vector3 var _cast_arrival_position: Vector3
var _cast_tracks_water_surface: bool = false
var _bobber_surface_position: Vector3 var _bobber_surface_position: Vector3
var _bobber_idle_elapsed: float = 0.0 var _bobber_idle_elapsed: float = 0.0
var _active_tween: Tween var _active_tween: Tween
@ -68,6 +73,9 @@ func _process(delta: float) -> void:
if _mode == VisualMode.FISHING and _bobber.visible: if _mode == VisualMode.FISHING and _bobber.visible:
_bobber_idle_elapsed += delta _bobber_idle_elapsed += delta
_apply_bobber_idle_motion() _apply_bobber_idle_motion()
var bobber_position := _bobber.global_position
bobber_position.y += WaterSurfaceMotionType.get_default_height_offset()
_bobber.global_position = bobber_position
if _line_mode != LineMode.HIDDEN: if _line_mode != LineMode.HIDDEN:
_update_line(delta) _update_line(delta)
@ -154,6 +162,7 @@ func begin_cast(
_cast_arrival_position = ( _cast_arrival_position = (
blocked_landing_position if cast_is_blocked else target blocked_landing_position if cast_is_blocked else target
) )
_cast_tracks_water_surface = not cast_is_blocked
var cast_start: Vector3 = _rod_tip.global_position var cast_start: Vector3 = _rod_tip.global_position
_bobber.global_position = cast_start _bobber.global_position = cast_start
@ -289,6 +298,7 @@ func cleanup() -> void:
_bobber.scale = Vector3.ONE _bobber.scale = Vector3.ONE
_bobber.rotation = Vector3.ZERO _bobber.rotation = Vector3.ZERO
_cast_arrival_position = Vector3.ZERO _cast_arrival_position = Vector3.ZERO
_cast_tracks_water_surface = false
_bobber_surface_position = Vector3.ZERO _bobber_surface_position = Vector3.ZERO
_bobber_idle_elapsed = 0.0 _bobber_idle_elapsed = 0.0
set_line_mode(LineMode.HIDDEN) set_line_mode(LineMode.HIDDEN)
@ -306,6 +316,11 @@ func _set_cast_sample(
) -> void: ) -> void:
var sample: Vector3 = start.lerp(target, progress) var sample: Vector3 = start.lerp(target, progress)
sample.y += sin(progress * PI) * cast_arc_height sample.y += sin(progress * PI) * cast_arc_height
if _cast_tracks_water_surface:
sample.y += (
WaterSurfaceMotionType.get_default_height_offset()
* smoothstep(0.72, 1.0, progress)
)
_bobber.global_position = sample _bobber.global_position = sample

View file

@ -100,14 +100,14 @@ enum FishingState {
@export_range(0.0, 1.0, 0.01) var withdrawal_surface_clearance: float = 0.4 @export_range(0.0, 1.0, 0.01) var withdrawal_surface_clearance: float = 0.4
@export_category("Timing") @export_category("Timing")
const BITE_QUICK_MIN_SECONDS: float = 8.0 const BITE_QUICK_MIN_SECONDS: float = 6.0
const BITE_QUICK_MAX_SECONDS: float = 24.0 const BITE_QUICK_MAX_SECONDS: float = 18.0
const BITE_TYPICAL_MAX_SECONDS: float = 72.0 const BITE_TYPICAL_MAX_SECONDS: float = 48.0
const BITE_LONG_MAX_SECONDS: float = 144.0 const BITE_LONG_MAX_SECONDS: float = 72.0
const BITE_MAX_SECONDS: float = 192.0 const BITE_MAX_SECONDS: float = 90.0
const BITE_QUICK_PROBABILITY: float = 0.25 const BITE_QUICK_PROBABILITY: float = 0.30
const BITE_TYPICAL_PROBABILITY: float = 0.65 const BITE_TYPICAL_PROBABILITY: float = 0.65
const BITE_LONG_PROBABILITY: float = 0.08 const BITE_LONG_PROBABILITY: float = 0.04
const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1 const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0 @export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0

View file

@ -4,6 +4,9 @@ extends RefCounted
const FishableWaterRegionType = preload( const FishableWaterRegionType = preload(
"res://world/fishable_water_region.gd" "res://world/fishable_water_region.gd"
) )
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
var has_surface: bool = false var has_surface: bool = false
var position: Vector3 = Vector3.ZERO var position: Vector3 = Vector3.ZERO
@ -19,7 +22,12 @@ func is_fishable() -> bool:
func get_marker_position(vertical_offset: float) -> Vector3: func get_marker_position(vertical_offset: float) -> Vector3:
if not has_surface: if not has_surface:
return position return position
return position + normal.normalized() * vertical_offset var presentation_offset := vertical_offset
if is_water_surface:
presentation_offset += (
WaterSurfaceMotionType.get_default_height_offset()
)
return position + normal.normalized() * presentation_offset
func get_bobber_position(solid_clearance: float) -> Vector3: func get_bobber_position(solid_clearance: float) -> Vector3:

View file

@ -1,6 +1,10 @@
class_name RemoteFishingPresentation class_name RemoteFishingPresentation
extends Node3D extends Node3D
const WaterSurfaceMotionType = preload(
"res://world/water_surface_motion.gd"
)
signal return_completed signal return_completed
var _owner: Player var _owner: Player
@ -140,6 +144,10 @@ func _set_cast_sample(
) -> void: ) -> void:
_target = start.lerp(target, progress) _target = start.lerp(target, progress)
_target.y += sin(progress * PI) * 2.0 _target.y += sin(progress * PI) * 2.0
_target.y += (
WaterSurfaceMotionType.get_default_height_offset()
* smoothstep(0.72, 1.0, progress)
)
_bobber.global_position = _target _bobber.global_position = _target
@ -152,7 +160,10 @@ func _on_cast_finished() -> void:
func _apply_bobber_idle_motion() -> void: func _apply_bobber_idle_motion() -> void:
var phase: float = _bobber_idle_elapsed * 0.7 * TAU var phase: float = _bobber_idle_elapsed * 0.7 * TAU
_target = _pending_target + Vector3.UP * sin(phase) * 0.035 _target = _pending_target + Vector3.UP * (
WaterSurfaceMotionType.get_default_height_offset()
+ sin(phase) * 0.035
)
_bobber.global_position = _target _bobber.global_position = _target
_bobber.rotation.z = deg_to_rad(4.0) * sin(phase * 0.73) _bobber.rotation.z = deg_to_rad(4.0) * sin(phase * 0.73)

View file

@ -5,10 +5,15 @@ const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd") const ItemDataType = preload("res://items/item_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd") const OwnedItemType = preload("res://items/owned_item.gd")
const DEFAULT_UNLOCKED_BAIT_IDS: Array[StringName] = [&"worms"]
signal contents_changed signal contents_changed
var _catalog: ItemCatalogType var _catalog: ItemCatalogType
var _items: Array[OwnedItemType] = [] var _items: Array[OwnedItemType] = []
var _unlocked_bait_ids: Array[StringName] = (
DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
)
func setup(catalog: ItemCatalogType) -> void: func setup(catalog: ItemCatalogType) -> void:
@ -27,6 +32,8 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool:
owned.item_id = item_id owned.item_id = item_id
owned.quantity = quantity owned.quantity = quantity
_items.append(owned) _items.append(owned)
if item.is_bait() and not _unlocked_bait_ids.has(item_id):
_unlocked_bait_ids.append(item_id)
contents_changed.emit() contents_changed.emit()
return true return true
@ -85,6 +92,46 @@ func get_all_items() -> Array[OwnedItemType]:
return result return result
func get_unlocked_bait_ids() -> Array[StringName]:
return _unlocked_bait_ids.duplicate()
func is_bait_unlocked(item_id: StringName) -> bool:
return _unlocked_bait_ids.has(item_id)
func get_unlocked_bait_items() -> Array[OwnedItemType]:
var result: Array[OwnedItemType] = []
for item_id: StringName in _unlocked_bait_ids:
var owned: OwnedItemType = get_owned_item(item_id)
if owned != null:
result.append(owned.duplicate_record())
continue
var empty_record := OwnedItemType.new()
empty_record.item_id = item_id
empty_record.quantity = 0
result.append(empty_record)
return result
func replace_unlocked_bait_ids(item_ids: Array[StringName]) -> bool:
var validated: Array[StringName] = DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
var seen: Dictionary[StringName, bool] = {}
for item_id: StringName in item_ids:
if item_id.is_empty() or seen.has(item_id):
return false
seen[item_id] = true
if validated.has(item_id):
continue
var item: ItemDataType = _resolve_valid_item(item_id)
if item == null or not item.is_bait():
return false
validated.append(item_id)
_unlocked_bait_ids = validated
contents_changed.emit()
return true
func replace_all_items(items: Array[OwnedItemType]) -> bool: func replace_all_items(items: Array[OwnedItemType]) -> bool:
var validated: Array[OwnedItemType] = [] var validated: Array[OwnedItemType] = []
var seen: Dictionary[StringName, bool] = {} var seen: Dictionary[StringName, bool] = {}

View file

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

View file

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

14
items/catalog/shrimp.tres Normal file
View file

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

14
items/catalog/snails.tres Normal file
View file

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

View file

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

View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"whole_anchovy"
display_name = "whole anchovy"
description = "premium whole bait with high rare catch potential."
category = 2
bait_tags = Array[StringName]([&"bait_rarity_4"])
stackable = true
max_stack = 10
hotbar_allowed = false

View file

@ -0,0 +1,14 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[resource]
script = ExtResource("1")
item_id = &"whole_sardine"
display_name = "whole sardine"
description = "valuable whole bait tuned for exceptional catches."
category = 2
bait_tags = Array[StringName]([&"bait_rarity_5"])
stackable = true
max_stack = 10
hotbar_allowed = false

View file

@ -1,14 +1,16 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3] [gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"] [ext_resource type="Script" path="res://items/item_data.gd" id="1"]
[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_worms.png" id="2_icon"]
[resource] [resource]
script = ExtResource("1") script = ExtResource("1")
item_id = &"worms" item_id = &"worms"
display_name = "worms" display_name = "worms"
description = "common bait. fills your tacklebox at the fishing shop." description = "basic bait that opens a small chance for rarer catches."
icon = ExtResource("2_icon")
category = 2 category = 2
bait_tags = Array[StringName]([&"worm"]) bait_tags = Array[StringName]([&"worm", &"bait_rarity_0"])
stackable = true stackable = true
max_stack = 10 max_stack = 10
hotbar_allowed = false hotbar_allowed = false

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -2,20 +2,20 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://b2ce1ikmnbktx" uid="uid://bn6815x0rrspw"
path="res://.godot/imported/dirt.png-ca5bf3f3fbf583edaa719ac8c686549d.ctex" path="res://.godot/imported/64_bait_worms.png-c9f31d77527e73888fc56963d4e78e30.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://art/exported/environment/textures/dirt.png" source_file="res://items/icons/bait/64_bait_worms.png"
dest_files=["res://.godot/imported/dirt.png-ca5bf3f3fbf583edaa719ac8c686549d.ctex"] dest_files=["res://.godot/imported/64_bait_worms.png-c9f31d77527e73888fc56963d4e78e30.ctex"]
[params] [params]
compress/mode=2 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0 compress/uastc_level=0
@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""
@ -37,5 +37,4 @@ process/normal_map_invert_y=false
process/hdr_as_srgb=false process/hdr_as_srgb=false
process/hdr_clamp_exposure=false process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=0 detect_3d/compress_to=1
import_profile="managed_default"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -2,20 +2,20 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://b4n8bf4cgua4s" uid="uid://baht2hredfky8"
path="res://.godot/imported/grass.png-4d1108b11a421d9c16bd72fb0bc993d3.ctex" path="res://.godot/imported/64_cooler_plus.png-d0daa39246f92fd9774ec1721970efad.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://art/exported/environment/textures/grass.png" source_file="res://items/icons/equipment/64_cooler_plus.png"
dest_files=["res://.godot/imported/grass.png-4d1108b11a421d9c16bd72fb0bc993d3.ctex"] dest_files=["res://.godot/imported/64_cooler_plus.png-d0daa39246f92fd9774ec1721970efad.ctex"]
[params] [params]
compress/mode=2 compress/mode=0
compress/high_quality=false compress/high_quality=false
compress/lossy_quality=0.7 compress/lossy_quality=0.7
compress/uastc_level=0 compress/uastc_level=0
@ -23,7 +23,7 @@ compress/rdo_quality_loss=0.0
compress/hdr_compression=1 compress/hdr_compression=1
compress/normal_map=0 compress/normal_map=0
compress/channel_pack=0 compress/channel_pack=0
mipmaps/generate=true mipmaps/generate=false
mipmaps/limit=-1 mipmaps/limit=-1
roughness/mode=0 roughness/mode=0
roughness/src_normal="" roughness/src_normal=""
@ -37,5 +37,4 @@ process/normal_map_invert_y=false
process/hdr_as_srgb=false process/hdr_as_srgb=false
process/hdr_clamp_exposure=false process/hdr_clamp_exposure=false
process/size_limit=0 process/size_limit=0
detect_3d/compress_to=0 detect_3d/compress_to=1
import_profile="managed_default"

View file

@ -36,7 +36,7 @@ var _pending_rewards: Array[Dictionary] = []
var _lifetime_claimed: Array[String] = [] var _lifetime_claimed: Array[String] = []
var _total_catches: int = 0 var _total_catches: int = 0
var _total_sold: int = 0 var _total_sold: int = 0
var _previous_hour: float = WorldTimeService.DEFAULT_START_HOUR var _daily_clock_hour: float = WorldTimeService.DEFAULT_START_HOUR
func setup( func setup(
@ -55,8 +55,8 @@ func setup(
_world_time = world_time _world_time = world_time
_world_weather = world_weather _world_weather = world_weather
_session = session _session = session
_previous_hour = _world_time.get_time_hours() _daily_clock_hour = _world_time.get_time_hours()
_world_time.time_changed.connect(_on_time_changed) _world_time.natural_time_advanced.connect(_on_natural_time_advanced)
_session.state_changed.connect(_on_session_state_changed) _session.state_changed.connect(_on_session_state_changed)
_collection.collection_changed.connect(_on_current_state_changed) _collection.collection_changed.connect(_on_current_state_changed)
_experience.experience_changed.connect(_on_experience_changed) _experience.experience_changed.connect(_on_experience_changed)
@ -86,7 +86,6 @@ func set_save_manager(save_manager: PlayerSaveManager) -> void:
func begin_progression_session() -> void: func begin_progression_session() -> void:
_progression_ready = true _progression_ready = true
_previous_hour = _world_time.get_time_hours()
if _session.is_host(): if _session.is_host():
_activate_host_board() _activate_host_board()
changed.emit() changed.emit()
@ -155,7 +154,7 @@ func get_time_until_refresh_text() -> String:
if _active_plan_id.is_empty() or _world_time == null: if _active_plan_id.is_empty() or _world_time == null:
return "daily jobs unavailable" return "daily jobs unavailable"
var elapsed: float = fposmod( var elapsed: float = fposmod(
_world_time.get_time_hours() - DAILY_REFRESH_HOUR, _daily_clock_hour - DAILY_REFRESH_HOUR,
WorldTimeService.HOURS_PER_DAY, WorldTimeService.HOURS_PER_DAY,
) )
var hours_remaining: float = WorldTimeService.HOURS_PER_DAY - elapsed var hours_remaining: float = WorldTimeService.HOURS_PER_DAY - elapsed
@ -185,7 +184,10 @@ func claim(claim_id: String) -> bool:
func get_host_board_network_data() -> Dictionary: func get_host_board_network_data() -> Dictionary:
return _host_board.duplicate(true) var board := _host_board.duplicate(true)
if not board.is_empty():
board["daily_clock_hour"] = _daily_clock_hour
return board
func apply_remote_board(board: Dictionary) -> bool: func apply_remote_board(board: Dictionary) -> bool:
@ -198,6 +200,8 @@ func apply_remote_board(board: Dictionary) -> bool:
_daily_progress.clear() _daily_progress.clear()
_daily_completions.clear() _daily_completions.clear()
_active_board = board.duplicate(true) _active_board = board.duplicate(true)
if board.has("daily_clock_hour"):
_daily_clock_hour = float(board["daily_clock_hour"])
changed.emit() changed.emit()
return true return true
@ -223,6 +227,7 @@ func to_save_data() -> Dictionary:
"active_plan_id": _active_plan_id, "active_plan_id": _active_plan_id,
"daily_progress": progress, "daily_progress": progress,
"daily_completions": completions, "daily_completions": completions,
"daily_clock_hour": _daily_clock_hour,
"pending_rewards": _pending_rewards.duplicate(true), "pending_rewards": _pending_rewards.duplicate(true),
"lifetime_claimed": _lifetime_claimed.duplicate(), "lifetime_claimed": _lifetime_claimed.duplicate(),
"statistics": { "statistics": {
@ -246,6 +251,12 @@ func restore_from_save_data(data: Dictionary) -> bool:
var completions: Dictionary = data.get("daily_completions", {}) var completions: Dictionary = data.get("daily_completions", {})
for key: Variant in completions: for key: Variant in completions:
_daily_completions[str(key)] = mini(int(completions[key]), 1) _daily_completions[str(key)] = mini(int(completions[key]), 1)
_daily_clock_hour = float(data.get(
"daily_clock_hour",
_world_time.get_time_hours()
if _world_time != null
else WorldTimeService.DEFAULT_START_HOUR,
))
_pending_rewards.clear() _pending_rewards.clear()
var rewards: Array = data.get("pending_rewards", []) var rewards: Array = data.get("pending_rewards", [])
var restored_daily_rewards: Dictionary[String, bool] = {} var restored_daily_rewards: Dictionary[String, bool] = {}
@ -290,6 +301,7 @@ func reset_to_defaults() -> void:
_lifetime_claimed.clear() _lifetime_claimed.clear()
_total_catches = 0 _total_catches = 0
_total_sold = 0 _total_sold = 0
_daily_clock_hour = WorldTimeService.DEFAULT_START_HOUR
changed.emit() changed.emit()
@ -300,6 +312,7 @@ static func default_save_data() -> Dictionary:
"active_plan_id": "", "active_plan_id": "",
"daily_progress": {}, "daily_progress": {},
"daily_completions": {}, "daily_completions": {},
"daily_clock_hour": WorldTimeService.DEFAULT_START_HOUR,
"pending_rewards": [], "pending_rewards": [],
"lifetime_claimed": [], "lifetime_claimed": [],
"statistics": {"fish_caught": 0, "fish_sold": 0}, "statistics": {"fish_caught": 0, "fish_sold": 0},
@ -363,6 +376,16 @@ static func validate_save_data(value: Variant) -> bool:
var host_board: Dictionary = data.get("host_board", {}) var host_board: Dictionary = data.get("host_board", {})
if not host_board.is_empty() and not validate_board(host_board): if not host_board.is_empty() and not validate_board(host_board):
return false return false
if (
data.has("daily_clock_hour")
and (
typeof(data["daily_clock_hour"]) not in [TYPE_FLOAT, TYPE_INT]
or not is_finite(float(data["daily_clock_hour"]))
or float(data["daily_clock_hour"]) < 0.0
or float(data["daily_clock_hour"]) >= WorldTimeService.HOURS_PER_DAY
)
):
return false
var progress: Dictionary = data.get("daily_progress", {}) var progress: Dictionary = data.get("daily_progress", {})
if progress.size() > 8: if progress.size() > 8:
return false return false
@ -475,19 +498,22 @@ func _apply_host_weather_plan() -> void:
) )
func _on_time_changed( func _on_natural_time_advanced(hours: float) -> void:
time_hours: float, if not is_finite(hours) or hours <= 0.0:
_phase: WorldTimeService.Phase, return
) -> void: var previous_hour := _daily_clock_hour
_daily_clock_hour = fposmod(
_daily_clock_hour + hours,
WorldTimeService.HOURS_PER_DAY,
)
if ( if (
_progression_ready _progression_ready
and _session != null and _session != null
and _session.is_host() and _session.is_host()
and _crossed_daily_refresh(_previous_hour, time_hours) and _crossed_daily_refresh(previous_hour, _daily_clock_hour)
): ):
var cycle: int = int(_host_board.get("cycle", -1)) + 1 var cycle: int = int(_host_board.get("cycle", -1)) + 1
_generate_host_board(maxi(cycle, 0)) _generate_host_board(maxi(cycle, 0))
_previous_hour = time_hours
func _on_session_state_changed(state: NetworkSession.State) -> void: func _on_session_state_changed(state: NetworkSession.State) -> void:

View file

@ -91,6 +91,7 @@ const WorldWeatherServiceType = preload("res://world/world_weather_service.gd")
const NetworkWorldWeatherServiceType = preload( const NetworkWorldWeatherServiceType = preload(
"res://network/network_world_weather_service.gd" "res://network/network_world_weather_service.gd"
) )
const RainAmbienceType = preload("res://world/rain_ambience.gd")
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd") const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
const NetworkJobServiceType = preload("res://network/network_job_service.gd") const NetworkJobServiceType = preload("res://network/network_job_service.gd")
@ -210,10 +211,15 @@ var _pending_existing_root_path := ""
var _local_recovery_attempt_id: String = "" var _local_recovery_attempt_id: String = ""
@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience @onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience
var _rain_ambience: RainAmbienceType
func _ready() -> void: func _ready() -> void:
DisplayServer.window_set_title("NETfishing") DisplayServer.window_set_title("NETfishing")
_rain_ambience = RainAmbienceType.new()
_rain_ambience.name = "RainAmbience"
add_child(_rain_ambience)
_rain_ambience.configure(_world_weather)
_shoreline_ambience.configure( _shoreline_ambience.configure(
_player, _player,
_test_world.get_saltwater_shoreline_mesh(), _test_world.get_saltwater_shoreline_mesh(),
@ -1096,7 +1102,16 @@ func _unhandled_input(event: InputEvent) -> void:
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt()) var show_shop_prompt := _can_show_shop_prompt()
var shop_prompt_anchor := (
_shop_interaction.get_prompt_anchor_position()
if _shop_interaction != null
else Vector3.ZERO
)
_game_ui.set_shop_prompt_visible(
show_shop_prompt,
shop_prompt_anchor,
)
func _apply_runtime_settings(settings: PlayerSettingsType) -> void: func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
@ -1151,6 +1166,7 @@ func _reset_pixelation() -> void:
func _set_gameplay_active(active: bool) -> void: func _set_gameplay_active(active: bool) -> void:
_gameplay_started = active _gameplay_started = active
_shoreline_ambience.set_active(active) _shoreline_ambience.set_active(active)
_rain_ambience.set_active(active)
_title_background.visible = not active _title_background.visible = not active
_world_pixelation.set_gameplay_active(active) _world_pixelation.set_gameplay_active(active)
_ui_pixelation.set_gameplay_active(active) _ui_pixelation.set_gameplay_active(active)

File diff suppressed because one or more lines are too long

View file

@ -79,6 +79,21 @@ func send_local_message(body: String) -> bool:
return true return true
func broadcast_system_message(body: String) -> bool:
if _session == null or not _session.is_host():
return false
var clean := NetworkChatProtocol.sanitize_body(body)
if clean.is_empty():
return false
_broadcast(_make_message(
NetworkChatProtocol.Kind.SYSTEM,
0,
"",
clean,
))
return true
func get_history() -> Array[Dictionary]: func get_history() -> Array[Dictionary]:
var result: Array[Dictionary] = [] var result: Array[Dictionary] = []
for message: Dictionary in _history: for message: Dictionary in _history:

View file

@ -992,6 +992,7 @@ func _capture_assets() -> Dictionary:
return { return {
"wallet": _wallet.get_balance(), "wallet": _wallet.get_balance(),
"bag": _bag.get_all_items(), "bag": _bag.get_all_items(),
"unlocked_bait_ids": _bag.get_unlocked_bait_ids(),
"catches": _inventory.get_all_catches(), "catches": _inventory.get_all_catches(),
"next_sequence": _inventory.get_next_catch_sequence(), "next_sequence": _inventory.get_next_catch_sequence(),
"discovered": _collection_log.get_discovered_ids(), "discovered": _collection_log.get_discovered_ids(),
@ -1002,6 +1003,7 @@ func _capture_assets() -> Dictionary:
func _restore_assets(snapshot: Dictionary) -> void: func _restore_assets(snapshot: Dictionary) -> void:
_wallet.restore_balance(int(snapshot["wallet"])) _wallet.restore_balance(int(snapshot["wallet"]))
_bag.replace_all_items(snapshot["bag"]) _bag.replace_all_items(snapshot["bag"])
_bag.replace_unlocked_bait_ids(snapshot["unlocked_bait_ids"])
_inventory.replace_all_catches( _inventory.replace_all_catches(
snapshot["catches"], int(snapshot["next_sequence"]) snapshot["catches"], int(snapshot["next_sequence"])
) )

View file

@ -2,8 +2,13 @@ class_name NetworkProfilePreferences
extends Node extends Node
const FORMAT_VERSION: int = 1 const FORMAT_VERSION: int = 1
const VoiceProfilesType = preload(
"res://player/animalese_voice_profiles.gd"
)
var profile_id: String = "" var profile_id: String = ""
var display_name: String = "Player" var display_name: String = "Player"
var voice_id: String = VoiceProfilesType.DEFAULT_ID
var speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID
var created_at_unix: int = 0 var created_at_unix: int = 0
var _profile_path := "" var _profile_path := ""
var _expected_hash := "" var _expected_hash := ""
@ -48,19 +53,39 @@ func load_or_create() -> bool:
Time.get_ticks_usec(), Time.get_ticks_usec(),
] ]
display_name = "Player" display_name = "Player"
voice_id = VoiceProfilesType.DEFAULT_ID
speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID
created_at_unix = int(Time.get_unix_time_from_system()) created_at_unix = int(Time.get_unix_time_from_system())
return _save_atomic() return _save_atomic()
func set_display_name(value: String) -> bool: func set_display_name(value: String) -> bool:
return set_profile_identity(value, voice_id, speech_speed_id)
func set_profile_identity(
value: String,
selected_voice_id: String,
selected_speech_speed_id: String,
) -> bool:
var clean_name: String = value.strip_edges() var clean_name: String = value.strip_edges()
if not is_valid_display_name(clean_name): if (
not is_valid_display_name(clean_name)
or not VoiceProfilesType.is_valid(selected_voice_id)
or not VoiceProfilesType.is_valid_speed(selected_speech_speed_id)
):
return false return false
var previous: String = display_name var previous_name: String = display_name
var previous_voice_id: String = voice_id
var previous_speech_speed_id: String = speech_speed_id
display_name = clean_name display_name = clean_name
voice_id = selected_voice_id
speech_speed_id = selected_speech_speed_id
if _save_atomic(): if _save_atomic():
return true return true
display_name = previous display_name = previous_name
voice_id = previous_voice_id
speech_speed_id = previous_speech_speed_id
return false return false
@ -109,6 +134,12 @@ func _load_existing() -> bool:
return false return false
profile_id = loaded_id profile_id = loaded_id
display_name = loaded_name display_name = loaded_name
voice_id = VoiceProfilesType.sanitized_id(
str(data.get("voice_id", VoiceProfilesType.DEFAULT_ID))
)
speech_speed_id = VoiceProfilesType.sanitized_speed_id(
str(data.get("speech_speed_id", VoiceProfilesType.DEFAULT_SPEED_ID))
)
created_at_unix = int(data["created_at_unix"]) created_at_unix = int(data["created_at_unix"])
return true return true
@ -118,6 +149,8 @@ func _save_atomic() -> bool:
"format_version": FORMAT_VERSION, "format_version": FORMAT_VERSION,
"profile_id": profile_id, "profile_id": profile_id,
"display_name": display_name, "display_name": display_name,
"voice_id": voice_id,
"speech_speed_id": speech_speed_id,
"created_at_unix": created_at_unix, "created_at_unix": created_at_unix,
} }
var result := PortableFileGuard.write_guarded( var result := PortableFileGuard.write_guarded(

View file

@ -1,6 +1,10 @@
class_name NetworkProfileService class_name NetworkProfileService
extends Node extends Node
const VoiceProfilesType = preload(
"res://player/animalese_voice_profiles.gd"
)
signal conflict_result( signal conflict_result(
request_id: String, request_id: String,
has_conflict: bool, has_conflict: bool,
@ -18,6 +22,8 @@ var _preferences: NetworkProfilePreferences
var _appearance_store: PlayerAppearanceStore var _appearance_store: PlayerAppearanceStore
var _spawn_service: PlayerSpawnService var _spawn_service: PlayerSpawnService
var _pending_apply: Dictionary[String, Dictionary] = {} var _pending_apply: Dictionary[String, Dictionary] = {}
var _pending_voice_ids: Dictionary[String, String] = {}
var _pending_speech_speed_ids: Dictionary[String, String] = {}
var _host_pending_apply: Dictionary[String, Dictionary] = {} var _host_pending_apply: Dictionary[String, Dictionary] = {}
var _latest_check_id: String = "" var _latest_check_id: String = ""
var _latest_check_name: String = "" var _latest_check_name: String = ""
@ -40,6 +46,7 @@ func setup(
_session.join_authenticated.connect(_on_join_authenticated) _session.join_authenticated.connect(_on_join_authenticated)
_session.state_changed.connect(_on_session_state_changed) _session.state_changed.connect(_on_session_state_changed)
_apply_to_avatar(1, _appearance_store.get_snapshot()) _apply_to_avatar(1, _appearance_store.get_snapshot())
_apply_local_voice_to_avatar()
func get_persisted_name() -> String: func get_persisted_name() -> String:
@ -50,6 +57,14 @@ func get_persisted_appearance() -> Dictionary:
return _appearance_store.get_snapshot() return _appearance_store.get_snapshot()
func get_persisted_voice_id() -> String:
return _preferences.voice_id
func get_persisted_speech_speed_id() -> String:
return _preferences.speech_speed_id
func get_identity_fingerprint() -> String: func get_identity_fingerprint() -> String:
return ( return (
_session.get_local_identity_fingerprint() _session.get_local_identity_fingerprint()
@ -87,11 +102,15 @@ func apply_profile(
display_name: String, display_name: String,
appearance: Dictionary, appearance: Dictionary,
use_anyway: bool, use_anyway: bool,
voice_id: String = VoiceProfilesType.DEFAULT_ID,
speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID,
) -> bool: ) -> bool:
var clean_name := display_name.strip_edges() var clean_name := display_name.strip_edges()
if ( if (
not NetworkProfilePreferences.is_valid_display_name(clean_name) not NetworkProfilePreferences.is_valid_display_name(clean_name)
or not CharacterCustomizationCatalog.validate_snapshot(appearance) or not CharacterCustomizationCatalog.validate_snapshot(appearance)
or not VoiceProfilesType.is_valid(voice_id)
or not VoiceProfilesType.is_valid_speed(speech_speed_id)
): ):
apply_finished.emit(false, "Check the player name and appearance choices.") apply_finished.emit(false, "Check the player name and appearance choices.")
return false return false
@ -108,6 +127,8 @@ func apply_profile(
"profile_update", NetworkProfileProtocol.signature_fields(request) "profile_update", NetworkProfileProtocol.signature_fields(request)
) )
_pending_apply[request_id] = request _pending_apply[request_id] = request
_pending_voice_ids[request_id] = voice_id
_pending_speech_speed_ids[request_id] = speech_speed_id
if _session == null or not _session.is_gameplay_session_active(): if _session == null or not _session.is_gameplay_session_active():
_apply_local_result(request_id, true, "", false, PackedStringArray()) _apply_local_result(request_id, true, "", false, PackedStringArray())
elif _session.is_host(): elif _session.is_host():
@ -264,20 +285,46 @@ func _apply_local_result(
return return
if not accepted: if not accepted:
_pending_apply.erase(request_id) _pending_apply.erase(request_id)
_pending_voice_ids.erase(request_id)
_pending_speech_speed_ids.erase(request_id)
conflict_result.emit(request_id, conflict, suggestions) conflict_result.emit(request_id, conflict, suggestions)
apply_finished.emit(false, message) apply_finished.emit(false, message)
return return
var previous_name := _preferences.display_name var previous_name := _preferences.display_name
if not _preferences.set_display_name(str(request["display_name"])): var previous_voice_id := _preferences.voice_id
var previous_speech_speed_id := _preferences.speech_speed_id
var requested_voice_id: String = _pending_voice_ids.get(
request_id,
VoiceProfilesType.DEFAULT_ID,
)
var requested_speech_speed_id: String = _pending_speech_speed_ids.get(
request_id,
VoiceProfilesType.DEFAULT_SPEED_ID,
)
if not _preferences.set_profile_identity(
str(request["display_name"]),
requested_voice_id,
requested_speech_speed_id,
):
_pending_apply.erase(request_id) _pending_apply.erase(request_id)
_pending_voice_ids.erase(request_id)
_pending_speech_speed_ids.erase(request_id)
apply_finished.emit(false, "Profile could not be saved.") apply_finished.emit(false, "Profile could not be saved.")
return return
if not _appearance_store.save_snapshot(request["appearance"]): if not _appearance_store.save_snapshot(request["appearance"]):
_preferences.set_display_name(previous_name) _preferences.set_profile_identity(
previous_name,
previous_voice_id,
previous_speech_speed_id,
)
_pending_apply.erase(request_id) _pending_apply.erase(request_id)
_pending_voice_ids.erase(request_id)
_pending_speech_speed_ids.erase(request_id)
apply_finished.emit(false, "Profile could not be saved.") apply_finished.emit(false, "Profile could not be saved.")
return return
_pending_apply.erase(request_id) _pending_apply.erase(request_id)
_pending_voice_ids.erase(request_id)
_pending_speech_speed_ids.erase(request_id)
if _session != null and _session.is_gameplay_session_active(): if _session != null and _session.is_gameplay_session_active():
if _session.is_host(): if _session.is_host():
_session.apply_canonical_profile( _session.apply_canonical_profile(
@ -304,6 +351,7 @@ func _apply_local_result(
_session.get_local_peer_id() if _session != null else 1, _session.get_local_peer_id() if _session != null else 1,
_appearance_store.get_snapshot(), _appearance_store.get_snapshot(),
) )
_apply_local_voice_to_avatar()
apply_finished.emit(true, "Profile saved.") apply_finished.emit(true, "Profile saved.")
@ -361,6 +409,7 @@ func _on_join_authenticated() -> void:
_apply_to_avatar( _apply_to_avatar(
_session.get_local_peer_id(), _appearance_store.get_snapshot() _session.get_local_peer_id(), _appearance_store.get_snapshot()
) )
_apply_local_voice_to_avatar()
func _on_peer_removed(_peer_id: int) -> void: func _on_peer_removed(_peer_id: int) -> void:
@ -400,6 +449,8 @@ func _refresh_latest_check() -> void:
func _on_session_state_changed(state: NetworkSession.State) -> void: func _on_session_state_changed(state: NetworkSession.State) -> void:
if state == NetworkSession.State.INACTIVE: if state == NetworkSession.State.INACTIVE:
_pending_apply.clear() _pending_apply.clear()
_pending_voice_ids.clear()
_pending_speech_speed_ids.clear()
_host_pending_apply.clear() _host_pending_apply.clear()
_latest_check_id = "" _latest_check_id = ""
_latest_check_name = "" _latest_check_name = ""
@ -465,5 +516,16 @@ func _apply_to_avatar(peer_id: int, appearance: Dictionary) -> void:
avatar.apply_appearance_snapshot(appearance) avatar.apply_appearance_snapshot(appearance)
func _apply_local_voice_to_avatar() -> void:
if _spawn_service == null:
return
var local_peer_id := (
_session.get_local_peer_id() if _session != null else 1
)
var avatar := _spawn_service.get_avatar(local_peer_id)
if avatar != null:
avatar.apply_animalese_voice_id(_preferences.voice_id)
func _new_id() -> String: func _new_id() -> String:
return Crypto.new().generate_random_bytes(16).hex_encode() return Crypto.new().generate_random_bytes(16).hex_encode()

View file

@ -56,6 +56,11 @@ static func validate_request(data: Variant) -> String:
var quantity: int = payload["quantity"] var quantity: int = payload["quantity"]
var wallet_balance: int = payload["wallet_balance"] var wallet_balance: int = payload["wallet_balance"]
var current_state: int = payload["current_state"] var current_state: int = payload["current_state"]
if (
payload.has("bait_unlocked")
and typeof(payload["bait_unlocked"]) != TYPE_BOOL
):
return "Purchase could not be completed."
if ( if (
request_id.is_empty() request_id.is_empty()
or request_id.length() > MAX_ID_LENGTH or request_id.length() > MAX_ID_LENGTH

View file

@ -109,7 +109,15 @@ func is_local_purchase_pending() -> bool:
func request_supply(item_id: StringName) -> String: func request_supply(item_id: StringName) -> String:
var owned: int = _bag.get_quantity(item_id) if _bag != null else 0 var owned: int = _bag.get_quantity(item_id) if _bag != null else 0
var quantity: int = FishingShopStockType.get_purchase_quantity(item_id, owned) var item: ItemDataType = (
_item_catalog.get_item_by_id(item_id)
if _item_catalog != null else null
)
var quantity: int = FishingShopStockType.get_purchase_quantity(
item_id,
owned,
item.max_stack if item != null else FishingShopStockType.WORM_MAX_STACK,
)
return _request_purchase( return _request_purchase(
item_id, item_id,
NetworkShopProtocol.ProductCategory.SUPPLY, NetworkShopProtocol.ProductCategory.SUPPLY,
@ -223,6 +231,13 @@ func _request_purchase(
"wallet_balance": _wallet.get_balance() if _wallet != null else 0, "wallet_balance": _wallet.get_balance() if _wallet != null else 0,
"current_state": current_state, "current_state": current_state,
} }
if (
category == NetworkShopProtocol.ProductCategory.SUPPLY
and FishingShopStockType.is_bait_topoff(product_id)
):
request["bait_unlocked"] = (
_bag != null and _bag.is_bait_unlocked(product_id)
)
_pending_local_request = request.duplicate(true) _pending_local_request = request.duplicate(true)
local_purchase_pending.emit(request_id) local_purchase_pending.emit(request_id)
if _session.is_host(): if _session.is_host():
@ -317,16 +332,18 @@ func _build_authoritative_result(
var item: ItemDataType = _item_catalog.get_item_by_id( var item: ItemDataType = _item_catalog.get_item_by_id(
product_id product_id
) if _item_catalog != null else null ) if _item_catalog != null else null
cost = FishingShopStockType.get_price(product_id) var bait_topoff: bool = (
FishingShopStockType.is_bait_topoff(product_id)
)
if ( if (
item == null item == null
or not item.is_valid() or not item.is_valid()
or product_id not in ( or product_id not in (
FishingShopStockType.get_stock_item_ids() FishingShopStockType.get_stock_item_ids()
) )
or (item.category != ItemDataType.Category.CONSUMABLE and not FishingShopStockType.is_bait_topoff(product_id)) or (item.category != ItemDataType.Category.CONSUMABLE and not bait_topoff)
or not item.stackable or not item.stackable
or (not item.usable and not FishingShopStockType.is_bait_topoff(product_id)) or (not item.usable and not bait_topoff)
or current_state >= item.max_stack or current_state >= item.max_stack
or current_state + quantity > item.max_stack or current_state + quantity > item.max_stack
): ):
@ -336,10 +353,21 @@ func _build_authoritative_result(
else "Purchase could not be completed." else "Purchase could not be completed."
) )
else: else:
if not FishingShopStockType.is_bait_topoff(product_id) and quantity != 1: var expected_quantity: int = (
FishingShopStockType.get_purchase_quantity(
product_id, current_state, item.max_stack
)
)
if quantity != expected_quantity:
rejection = "Purchase could not be completed." rejection = "Purchase could not be completed."
else: else:
cost *= quantity cost = FishingShopStockType.get_purchase_cost(
product_id,
quantity,
bool(request.get("bait_unlocked", true)),
)
if cost < 0:
rejection = "Purchase could not be completed."
resulting_state = current_state + quantity resulting_state = current_state + quantity
NetworkShopProtocol.ProductCategory.ROD: NetworkShopProtocol.ProductCategory.ROD:
rejection = "This item is not sold here." rejection = "This item is not sold here."
@ -532,6 +560,9 @@ func _apply_purchase_result(data: Dictionary) -> void:
return return
var wallet_snapshot: int = _wallet.get_balance() var wallet_snapshot: int = _wallet.get_balance()
var bag_snapshot: Array[OwnedItemType] = _bag.get_all_items() var bag_snapshot: Array[OwnedItemType] = _bag.get_all_items()
var bait_unlock_snapshot: Array[StringName] = (
_bag.get_unlocked_bait_ids()
)
var reel_snapshot: int = _upgrades.get_reel_speed_level() var reel_snapshot: int = _upgrades.get_reel_speed_level()
var barrier_snapshot: int = _upgrades.get_barrier_power_level() var barrier_snapshot: int = _upgrades.get_barrier_power_level()
var cooler_snapshot: int = _cooler_capacity.get_level() var cooler_snapshot: int = _cooler_capacity.get_level()
@ -539,6 +570,7 @@ func _apply_purchase_result(data: Dictionary) -> void:
var applied: bool = _apply_local_product(data) var applied: bool = _apply_local_product(data)
if not applied or not _save_manager.save_if_dirty(): if not applied or not _save_manager.save_if_dirty():
_bag.replace_all_items(bag_snapshot) _bag.replace_all_items(bag_snapshot)
_bag.replace_unlocked_bait_ids(bait_unlock_snapshot)
_upgrades.restore_levels(reel_snapshot, barrier_snapshot) _upgrades.restore_levels(reel_snapshot, barrier_snapshot)
_cooler_capacity.restore_level(cooler_snapshot) _cooler_capacity.restore_level(cooler_snapshot)
_art_unlocks.restore_mask(art_snapshot) _art_unlocks.restore_mask(art_snapshot)
@ -590,9 +622,22 @@ func _validate_local_result(data: Dictionary) -> String:
return "Not enough fish coin." return "Not enough fish coin."
match category: match category:
NetworkShopProtocol.ProductCategory.SUPPLY: NetworkShopProtocol.ProductCategory.SUPPLY:
if _bag.get_quantity(product_id) != expected_state: var item: ItemDataType = (
_item_catalog.get_item_by_id(product_id)
if _item_catalog != null else null
)
var bait_unlocked: bool = _bag.is_bait_unlocked(product_id)
if (
item == null
or _bag.get_quantity(product_id) != expected_state
or int(data["quantity"]) != FishingShopStockType.get_purchase_quantity(
product_id, expected_state, item.max_stack
)
):
return "Purchase could not be completed." return "Purchase could not be completed."
if cost != FishingShopStockType.get_price(product_id) * int(data["quantity"]): if cost != FishingShopStockType.get_purchase_cost(
product_id, int(data["quantity"]), bait_unlocked
):
return "Purchase could not be completed." return "Purchase could not be completed."
if not _bag.can_add_item(product_id, data["quantity"]): if not _bag.can_add_item(product_id, data["quantity"]):
return "Your Bag is full." return "Your Bag is full."

View file

@ -17,6 +17,12 @@ func setup(session: NetworkSession, world_time: WorldTimeService) -> void:
_world_time = world_time _world_time = world_time
_session.state_changed.connect(_on_session_state_changed) _session.state_changed.connect(_on_session_state_changed)
_session.peer_authenticated.connect(_on_peer_authenticated) _session.peer_authenticated.connect(_on_peer_authenticated)
if not _world_time.authoritative_time_set.is_connected(
_on_authoritative_time_set
):
_world_time.authoritative_time_set.connect(
_on_authoritative_time_set
)
set_process(true) set_process(true)
@ -82,6 +88,13 @@ func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
_send_snapshot(peer_id) _send_snapshot(peer_id)
func _on_authoritative_time_set(_time_hours: float) -> void:
if _session == null or not _session.is_host():
return
_sync_elapsed = 0.0
_broadcast_snapshot()
func _broadcast_snapshot() -> void: func _broadcast_snapshot() -> void:
if _session == null or not _session.is_host(): if _session == null or not _session.is_host():
return return

View file

@ -0,0 +1,57 @@
class_name AnimaleseVoiceProfiles
extends RefCounted
const DEFAULT_ID: String = "natural"
const DEFAULT_SPEED_ID: String = "normal"
const OPTIONS: Array[Dictionary] = [
{"id": "tiny", "label": "tiny", "pitch": 1.24},
{"id": "bright", "label": "bright", "pitch": 1.10},
{"id": "natural", "label": "natural", "pitch": 1.0},
{"id": "mellow", "label": "mellow", "pitch": 0.89},
{"id": "deep", "label": "deep", "pitch": 0.77},
]
const SPEED_OPTIONS: Array[Dictionary] = [
{"id": "slow", "label": "slow", "characters_per_second": 20.0},
{"id": "relaxed", "label": "relaxed", "characters_per_second": 24.0},
{"id": "normal", "label": "normal", "characters_per_second": 28.0},
{"id": "quick", "label": "quick", "characters_per_second": 34.0},
{"id": "rapid", "label": "rapid", "characters_per_second": 40.0},
]
static func is_valid(voice_id: String) -> bool:
for option: Dictionary in OPTIONS:
if str(option.get("id", "")) == voice_id:
return true
return false
static func sanitized_id(voice_id: String) -> String:
return voice_id if is_valid(voice_id) else DEFAULT_ID
static func pitch_for(voice_id: String) -> float:
var resolved_id := sanitized_id(voice_id)
for option: Dictionary in OPTIONS:
if str(option.get("id", "")) == resolved_id:
return float(option.get("pitch", 1.0))
return 1.0
static func is_valid_speed(speed_id: String) -> bool:
for option: Dictionary in SPEED_OPTIONS:
if str(option.get("id", "")) == speed_id:
return true
return false
static func sanitized_speed_id(speed_id: String) -> String:
return speed_id if is_valid_speed(speed_id) else DEFAULT_SPEED_ID
static func speed_for(speed_id: String) -> float:
var resolved_id := sanitized_speed_id(speed_id)
for option: Dictionary in SPEED_OPTIONS:
if str(option.get("id", "")) == resolved_id:
return float(option.get("characters_per_second", 28.0))
return 28.0

View file

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

View file

@ -50,6 +50,7 @@ const CONTROLLER_ZOOM_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
var appearance_snapshot: Dictionary = ( var appearance_snapshot: Dictionary = (
CharacterCustomizationCatalog.default_snapshot() CharacterCustomizationCatalog.default_snapshot()
) )
var animalese_voice_id: String = "natural"
var active_bait_id: StringName = StringName() var active_bait_id: StringName = StringName()
signal active_bait_changed(item_id: StringName) signal active_bait_changed(item_id: StringName)
@ -76,6 +77,14 @@ func apply_appearance_snapshot(snapshot: Dictionary) -> void:
appearance_snapshot = snapshot.duplicate(true) appearance_snapshot = snapshot.duplicate(true)
PlayerVisualPresenter.apply_appearance(_visuals, appearance_snapshot) PlayerVisualPresenter.apply_appearance(_visuals, appearance_snapshot)
func apply_animalese_voice_id(voice_id: String) -> void:
animalese_voice_id = voice_id
func get_animalese_voice_id() -> String:
return animalese_voice_id
class ShowcaseCameraSnapshot: class ShowcaseCameraSnapshot:
extends RefCounted extends RefCounted
@ -129,6 +138,8 @@ class ShowcaseCameraSnapshot:
@export var zoom_smoothing: float = 12.0 @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.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 @export_range(0.0, 1.0, 0.01) var controller_trigger_threshold: float = 0.55
@export_range(1.0, 40.0, 0.5) var free_camera_speed: float = 10.0
@export_range(1.0, 10.0, 0.5) var free_camera_sprint_multiplier: float = 3.0
@onready var _visuals: Node3D = %Visuals @onready var _visuals: Node3D = %Visuals
@onready var _character_rig: Node3D = get_node_or_null( @onready var _character_rig: Node3D = get_node_or_null(
@ -141,6 +152,7 @@ class ShowcaseCameraSnapshot:
@onready var _camera_pitch: Node3D = %CameraPitch @onready var _camera_pitch: Node3D = %CameraPitch
@onready var _spring_arm: SpringArm3D = %SpringArm3D @onready var _spring_arm: SpringArm3D = %SpringArm3D
@onready var _camera: Camera3D = %Camera3D @onready var _camera: Camera3D = %Camera3D
@onready var _player_collision_shape: CollisionShape3D = $CollisionShape3D
@onready var inventory: FishInventoryType = %Inventory @onready var inventory: FishInventoryType = %Inventory
@onready var collection_log: CollectionLogType = %CollectionLog @onready var collection_log: CollectionLogType = %CollectionLog
@onready var wallet: PlayerWalletType = %Wallet @onready var wallet: PlayerWalletType = %Wallet
@ -165,6 +177,11 @@ var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity"))
var _camera_dragging: bool = false var _camera_dragging: bool = false
var _camera_input_enabled: bool = true var _camera_input_enabled: bool = true
var _free_camera_active: bool = false
var _free_camera_body: CharacterBody3D
var _free_camera: Camera3D
var _free_camera_yaw: float = 0.0
var _free_camera_pitch: float = 0.0
var _movement_enabled: bool = true var _movement_enabled: bool = true
var _water_recovery_active: bool = false var _water_recovery_active: bool = false
var _remote_recovery_presentation_active: bool = false var _remote_recovery_presentation_active: bool = false
@ -197,6 +214,8 @@ var _network_target_visual_yaw: float = 0.0
var _network_snapshot_ready: bool = false var _network_snapshot_ready: bool = false
var _character_animation_name: StringName = &"" var _character_animation_name: StringName = &""
var _sitting: bool = false var _sitting: bool = false
var _sitting_intent_pending: bool = false
var _sitting_intent_sequence: int = -1
var _held_fish_visible: bool = false var _held_fish_visible: bool = false
var _showcase_animation_active: bool = false var _showcase_animation_active: bool = false
var _fishing_rod: Node3D var _fishing_rod: Node3D
@ -205,6 +224,8 @@ var _controller_mapping_manager: ControllerMappingManagerType
func _ready() -> void: func _ready() -> void:
if not bag.contents_changed.is_connected(_on_bag_contents_changed):
bag.contents_changed.connect(_on_bag_contents_changed)
PlayerVisualPresenter.apply_appearance(_visuals, appearance_snapshot) PlayerVisualPresenter.apply_appearance(_visuals, appearance_snapshot)
_initialize_fishing_rod() _initialize_fishing_rod()
_target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom) _target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom)
@ -213,6 +234,14 @@ func _ready() -> void:
_initialize_held_item_attachment() _initialize_held_item_attachment()
func _on_bag_contents_changed() -> void:
if (
not active_bait_id.is_empty()
and bag.get_quantity(active_bait_id) <= 0
):
unequip_bait()
func set_controller_mapping_manager( func set_controller_mapping_manager(
mapping_manager: ControllerMappingManagerType, mapping_manager: ControllerMappingManagerType,
) -> void: ) -> void:
@ -267,8 +296,18 @@ func _physics_process(delta: float) -> void:
if _network_interpolation_enabled: if _network_interpolation_enabled:
_update_network_interpolation(delta) _update_network_interpolation(delta)
return return
if local_control_enabled and _free_camera_active:
_update_free_camera_physics()
velocity.x = 0.0
velocity.z = 0.0
if not is_on_floor():
velocity.y -= _gravity * fall_gravity_multiplier * delta
move_and_slide()
_network_jump_pending = false
return
var jump_requested: bool = ( var jump_requested: bool = (
_movement_enabled _movement_enabled
and not _free_camera_active
and ( and (
(local_control_enabled and Input.is_action_just_pressed("jump")) (local_control_enabled and Input.is_action_just_pressed("jump"))
or (_network_authoritative_simulation and _network_jump_pending) or (_network_authoritative_simulation and _network_jump_pending)
@ -276,7 +315,7 @@ func _physics_process(delta: float) -> void:
) )
if _sitting: if _sitting:
if jump_requested: if jump_requested:
_set_sitting(false) _set_sitting(false, local_control_enabled)
else: else:
velocity = Vector3.ZERO velocity = Vector3.ZERO
_network_jump_pending = false _network_jump_pending = false
@ -370,6 +409,8 @@ func _process(delta: float) -> void:
) )
if not local_control_enabled: if not local_control_enabled:
return return
if _free_camera_active:
return
if ( if (
_camera_input_enabled _camera_input_enabled
@ -519,7 +560,7 @@ func toggle_sitting() -> void:
]) ])
): ):
return return
_set_sitting(should_sit) _set_sitting(should_sit, local_control_enabled)
func _has_any_character_animation(candidates: Array[StringName]) -> bool: func _has_any_character_animation(candidates: Array[StringName]) -> bool:
@ -531,10 +572,16 @@ func _has_any_character_animation(candidates: Array[StringName]) -> bool:
return false return false
func _set_sitting(should_sit: bool) -> void: func _set_sitting(
should_sit: bool,
is_local_intent: bool = false,
) -> void:
if _sitting == should_sit: if _sitting == should_sit:
return return
_sitting = should_sit _sitting = should_sit
if is_local_intent:
_sitting_intent_pending = true
_sitting_intent_sequence = -1
if _sitting: if _sitting:
velocity = Vector3.ZERO velocity = Vector3.ZERO
_character_animation_name = &"" _character_animation_name = &""
@ -548,6 +595,10 @@ func is_sitting() -> bool:
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if not local_control_enabled or not _camera_input_enabled: if not local_control_enabled or not _camera_input_enabled:
return return
if event.is_action_pressed("toggle_free_camera"):
_set_free_camera_active(not _free_camera_active)
get_viewport().set_input_as_handled()
return
if event.is_action("camera_drag"): if event.is_action("camera_drag"):
_camera_dragging = event.is_pressed() _camera_dragging = event.is_pressed()
@ -555,9 +606,14 @@ func _unhandled_input(event: InputEvent) -> void:
return return
if event is InputEventMouseMotion and _camera_dragging: if event is InputEventMouseMotion and _camera_dragging:
_rotate_camera(event.relative * mouse_sensitivity) if _free_camera_active:
_rotate_free_camera(event.relative * mouse_sensitivity)
else:
_rotate_camera(event.relative * mouse_sensitivity)
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
if _free_camera_active:
return
var mouse_zoom_in: bool = ( var mouse_zoom_in: bool = (
event is InputEventMouseButton event is InputEventMouseButton
@ -617,6 +673,98 @@ func _rotate_camera(delta_rotation: Vector2) -> void:
) )
func _set_free_camera_active(active: bool) -> void:
if _free_camera_active == active:
return
if active:
var camera_transform: Transform3D = _camera.global_transform
var duplicated_camera := _camera.duplicate() as Camera3D
if duplicated_camera == null:
return
var camera_body := CharacterBody3D.new()
camera_body.name = "FreeCameraBody"
camera_body.collision_layer = 0
camera_body.collision_mask = collision_mask
camera_body.motion_mode = CharacterBody3D.MOTION_MODE_FLOATING
add_child(camera_body)
camera_body.top_level = true
camera_body.global_position = camera_transform.origin
var camera_collision := CollisionShape3D.new()
camera_collision.name = "FreeCameraCollision"
camera_collision.shape = _player_collision_shape.shape
camera_body.add_child(camera_collision)
duplicated_camera.name = "FreeCamera"
duplicated_camera.unique_name_in_owner = false
camera_body.add_child(duplicated_camera)
duplicated_camera.transform = Transform3D(
camera_transform.basis,
Vector3.ZERO,
)
_free_camera_body = camera_body
_free_camera = duplicated_camera
var camera_rotation: Vector3 = _free_camera.global_rotation
_free_camera_yaw = camera_rotation.y
_free_camera_pitch = camera_rotation.x
_camera.current = false
_free_camera.current = true
_free_camera_active = true
return
_free_camera_active = false
_camera_dragging = false
if _free_camera != null:
_free_camera.current = false
_free_camera = null
if _free_camera_body != null:
_free_camera_body.queue_free()
_free_camera_body = null
_camera.current = local_control_enabled
func _update_free_camera_physics() -> void:
if _free_camera == null or _free_camera_body == null:
_set_free_camera_active(false)
return
var input_vector: Vector2 = Input.get_vector(
"move_left",
"move_right",
"move_forward",
"move_backward",
)
var vertical_input: float = (
Input.get_action_strength("jump")
- Input.get_action_strength("sneak")
)
var movement: Vector3 = (
_free_camera.global_basis.x * input_vector.x
+ _free_camera.global_basis.z * input_vector.y
+ Vector3.UP * vertical_input
)
if movement.length_squared() > 1.0:
movement = movement.normalized()
var movement_speed: float = free_camera_speed
if Input.is_action_pressed("sprint"):
movement_speed *= free_camera_sprint_multiplier
_free_camera_body.velocity = movement * movement_speed
_free_camera_body.move_and_slide()
func _rotate_free_camera(delta_rotation: Vector2) -> void:
if _free_camera == null:
return
_free_camera_yaw -= delta_rotation.x
var vertical_direction: float = -1.0 if invert_camera_y else 1.0
_free_camera_pitch = clampf(
_free_camera_pitch - delta_rotation.y * vertical_direction,
deg_to_rad(minimum_pitch_degrees),
deg_to_rad(maximum_pitch_degrees),
)
_free_camera.global_rotation = Vector3(
_free_camera_pitch,
_free_camera_yaw,
0.0,
)
func _set_target_zoom(value: float) -> void: func _set_target_zoom(value: float) -> void:
_target_zoom = clampf(value, minimum_zoom, maximum_zoom) _target_zoom = clampf(value, minimum_zoom, maximum_zoom)
@ -633,6 +781,8 @@ func _apply_axis_deadzone(value: float) -> float:
func set_local_control(enabled: bool) -> void: func set_local_control(enabled: bool) -> void:
if not enabled and _free_camera_active:
_set_free_camera_active(false)
local_control_enabled = enabled local_control_enabled = enabled
if is_node_ready(): if is_node_ready():
_camera.current = enabled _camera.current = enabled
@ -657,7 +807,9 @@ func configure_network_remote(authoritative_simulation: bool) -> void:
func capture_network_input(sequence: int) -> Dictionary: func capture_network_input(sequence: int) -> Dictionary:
if not _movement_enabled or _water_recovery_active: if _sitting_intent_pending and _sitting_intent_sequence < 0:
_sitting_intent_sequence = sequence
if not _movement_enabled or _water_recovery_active or _free_camera_active:
return { return {
"sequence": sequence, "sequence": sequence,
"axis": [0.0, 0.0], "axis": [0.0, 0.0],
@ -734,7 +886,17 @@ func apply_local_prediction_correction(snapshot: Dictionary) -> void:
var parsed: Dictionary = _parse_network_snapshot(snapshot) var parsed: Dictionary = _parse_network_snapshot(snapshot)
if parsed.is_empty(): if parsed.is_empty():
return return
_set_sitting(bool(parsed["sitting"])) var acknowledged_input: int = parsed["acknowledged_input"]
var sitting_intent_acknowledged: bool = (
_sitting_intent_pending
and _sitting_intent_sequence >= 0
and acknowledged_input >= _sitting_intent_sequence
)
if sitting_intent_acknowledged:
_sitting_intent_pending = false
_sitting_intent_sequence = -1
if not _sitting_intent_pending:
_set_sitting(bool(parsed["sitting"]))
var authoritative_position: Vector3 = parsed["position"] var authoritative_position: Vector3 = parsed["position"]
var error_distance: float = global_position.distance_to( var error_distance: float = global_position.distance_to(
authoritative_position authoritative_position
@ -780,6 +942,17 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
float(network_velocity[2]) float(network_velocity[2])
) )
var visual_yaw: float = float(snapshot.get("visual_yaw", 0.0)) var visual_yaw: float = float(snapshot.get("visual_yaw", 0.0))
if (
snapshot.has("acknowledged_input")
and (
typeof(snapshot.get("acknowledged_input")) != TYPE_INT
or int(snapshot.get("acknowledged_input")) < 0
)
):
return {}
var acknowledged_input: int = int(
snapshot.get("acknowledged_input", 0)
)
if snapshot.has("sitting") and typeof(snapshot.get("sitting")) != TYPE_BOOL: if snapshot.has("sitting") and typeof(snapshot.get("sitting")) != TYPE_BOOL:
return {} return {}
var sitting: bool = bool(snapshot.get("sitting", false)) var sitting: bool = bool(snapshot.get("sitting", false))
@ -793,6 +966,7 @@ func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
"position": parsed_position, "position": parsed_position,
"velocity": parsed_velocity, "velocity": parsed_velocity,
"visual_yaw": visual_yaw, "visual_yaw": visual_yaw,
"acknowledged_input": acknowledged_input,
"sitting": sitting, "sitting": sitting,
} }
@ -835,7 +1009,10 @@ func set_camera_input_enabled(enabled: bool) -> void:
func set_camera_active(active: bool) -> void: func set_camera_active(active: bool) -> void:
_camera.current = active if _free_camera_active and _free_camera != null:
_free_camera.current = active
else:
_camera.current = active
func apply_camera_settings( func apply_camera_settings(

View file

@ -43,6 +43,7 @@ class LoadSnapshot:
var wallet_balance: int = 0 var wallet_balance: int = 0
var next_catch_sequence: int = 1 var next_catch_sequence: int = 1
var bag_items: Array[OwnedItemType] = [] var bag_items: Array[OwnedItemType] = []
var unlocked_bait_ids: Array[StringName] = []
var hotbar_slots: Array[StringName] = [] var hotbar_slots: Array[StringName] = []
var fish_hotbar_slots: Array[StringName] = [] var fish_hotbar_slots: Array[StringName] = []
var selected_hotbar_slot: int = 0 var selected_hotbar_slot: int = 0
@ -258,7 +259,10 @@ func load_player_data() -> bool:
var wallet_restored: bool = _wallet.restore_balance( var wallet_restored: bool = _wallet.restore_balance(
snapshot.wallet_balance snapshot.wallet_balance
) )
var bag_restored: bool = _bag.replace_all_items(snapshot.bag_items) var bag_restored: bool = (
_bag.replace_all_items(snapshot.bag_items)
and _bag.replace_unlocked_bait_ids(snapshot.unlocked_bait_ids)
)
var hotbar_restored: bool = _hotbar.replace_state( var hotbar_restored: bool = _hotbar.replace_state(
snapshot.hotbar_slots, snapshot.hotbar_slots,
snapshot.selected_hotbar_slot, snapshot.selected_hotbar_slot,
@ -492,6 +496,12 @@ func _build_save_dictionary() -> Dictionary:
if owned == null or not owned.is_valid(): if owned == null or not owned.is_valid():
return {} return {}
serialized_items.append(owned.to_save_dict()) serialized_items.append(owned.to_save_dict())
var serialized_unlocked_baits: Array[String] = []
for item_id: StringName in _bag.get_unlocked_bait_ids():
var item_data = _item_catalog.get_item_by_id(item_id)
if item_data == null or not item_data.is_valid() or not item_data.is_bait():
return {}
serialized_unlocked_baits.append(String(item_id))
var serialized_slots: Array[String] = [] var serialized_slots: Array[String] = []
for item_id: StringName in _hotbar.get_slots(): for item_id: StringName in _hotbar.get_slots():
serialized_slots.append(String(item_id)) serialized_slots.append(String(item_id))
@ -524,6 +534,7 @@ func _build_save_dictionary() -> Dictionary:
}, },
"bag": { "bag": {
"items": serialized_items, "items": serialized_items,
"unlocked_bait_ids": serialized_unlocked_baits,
}, },
"hotbar": { "hotbar": {
"selected_slot": _hotbar.get_selected_slot(), "selected_slot": _hotbar.get_selected_slot(),
@ -589,6 +600,10 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
or typeof(inventory_data.get("catches")) != TYPE_ARRAY or typeof(inventory_data.get("catches")) != TYPE_ARRAY
or not inventory_data.has("next_catch_sequence") or not inventory_data.has("next_catch_sequence")
or typeof(bag_data.get("items")) != TYPE_ARRAY or typeof(bag_data.get("items")) != TYPE_ARRAY
or (
bag_data.has("unlocked_bait_ids")
and typeof(bag_data.get("unlocked_bait_ids")) != TYPE_ARRAY
)
or typeof(hotbar_data.get("slots")) != TYPE_ARRAY or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
or not hotbar_data.has("selected_slot") or not hotbar_data.has("selected_slot")
or not experience_data.has("total_experience") or not experience_data.has("total_experience")
@ -781,6 +796,34 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
seen_items[item_id] = true seen_items[item_id] = true
snapshot.bag_items.append(owned) snapshot.bag_items.append(owned)
var seen_unlocked_baits: Dictionary[StringName, bool] = {}
if bag_data.has("unlocked_bait_ids"):
var unlocked_bait_values: Array = bag_data["unlocked_bait_ids"]
for value: Variant in unlocked_bait_values:
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
continue
var bait_id: StringName = StringName(str(value))
var bait_data = _item_catalog.get_item_by_id(bait_id)
if (
bait_id.is_empty()
or seen_unlocked_baits.has(bait_id)
or bait_data == null
or not bait_data.is_valid()
or not bait_data.is_bait()
):
continue
seen_unlocked_baits[bait_id] = true
snapshot.unlocked_bait_ids.append(bait_id)
for owned: OwnedItemType in snapshot.bag_items:
var owned_item_data = _item_catalog.get_item_by_id(owned.item_id)
if (
owned_item_data != null
and owned_item_data.is_bait()
and not seen_unlocked_baits.has(owned.item_id)
):
seen_unlocked_baits[owned.item_id] = true
snapshot.unlocked_bait_ids.append(owned.item_id)
var slot_values: Array = hotbar_data["slots"] var slot_values: Array = hotbar_data["slots"]
snapshot.hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT) snapshot.hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
snapshot.hotbar_slots.fill(StringName()) snapshot.hotbar_slots.fill(StringName())
@ -1098,6 +1141,8 @@ func _restore_defaults() -> void:
) )
_wallet.restore_balance(0) _wallet.restore_balance(0)
_bag.replace_all_items(default_items) _bag.replace_all_items(default_items)
var default_unlocked_baits: Array[StringName] = []
_bag.replace_unlocked_bait_ids(default_unlocked_baits)
_hotbar.replace_state(default_slots, 0) _hotbar.replace_state(default_slots, 0)
_fishing_upgrades.reset_to_defaults() _fishing_upgrades.reset_to_defaults()
_cooler_capacity.reset_to_defaults() _cooler_capacity.reset_to_defaults()

View file

@ -0,0 +1,71 @@
extends SceneTree
const OUTPUT_DIRECTORY := "res://sound/dialogue/animalese/placeholder"
const SAMPLE_RATE: int = 22050
const SAMPLE_SECONDS: float = 0.055
const CHARACTERS := "abcdefghijklmnopqrstuvwxyz"
const VOWELS := "aeiou"
func _initialize() -> void:
var absolute_directory := ProjectSettings.globalize_path(OUTPUT_DIRECTORY)
var directory_error: Error = DirAccess.make_dir_recursive_absolute(
absolute_directory
)
if directory_error != OK:
push_error("Could not create animalese sample directory.")
quit(1)
return
for character_index: int in range(CHARACTERS.length()):
var character := CHARACTERS.substr(character_index, 1)
_save_sample(character, character_index)
_save_sample("fallback", CHARACTERS.length())
quit()
func _save_sample(sample_name: String, sample_index: int) -> void:
var stream := _build_sample(sample_name, sample_index)
var output_path := "%s/%s" % [OUTPUT_DIRECTORY, sample_name]
var save_error: Error = stream.save_to_wav(output_path)
if save_error != OK:
push_error("Could not save animalese sample: %s" % sample_name)
func _build_sample(sample_name: String, sample_index: int) -> AudioStreamWAV:
var frame_count := roundi(SAMPLE_RATE * SAMPLE_SECONDS)
var pcm := PackedByteArray()
pcm.resize(frame_count * 2)
var base_frequency := 245.0 + float(sample_index % 7) * 18.0
var formant_one := 620.0 + float(sample_index % 5) * 85.0
var formant_two := 1320.0 + float(sample_index % 6) * 115.0
var is_vowel := VOWELS.contains(sample_name)
var noise_seed := sample_index * 7919 + 104729
for frame_index: int in range(frame_count):
var time := float(frame_index) / SAMPLE_RATE
var attack := minf(time / 0.004, 1.0)
var release := minf((SAMPLE_SECONDS - time) / 0.014, 1.0)
var envelope := maxf(minf(attack, release), 0.0)
noise_seed = int(
(noise_seed * 1103515245 + 12345) & 0x7fffffff
)
var noise := float(noise_seed % 65536) / 32767.5 - 1.0
var voiced := (
sin(TAU * base_frequency * time) * 0.42
+ sin(TAU * base_frequency * 2.0 * time) * 0.16
)
var formants := (
sin(TAU * formant_one * time) * 0.18
+ sin(TAU * formant_two * time) * 0.10
)
var consonant_noise := noise * (0.08 if is_vowel else 0.24)
var value := envelope * (voiced + formants + consonant_noise) * 0.72
var signed_sample := clampi(roundi(value * 32767.0), -32768, 32767)
var encoded_sample := signed_sample if signed_sample >= 0 else signed_sample + 65536
pcm[frame_index * 2] = encoded_sample & 0xff
pcm[frame_index * 2 + 1] = (encoded_sample >> 8) & 0xff
var stream := AudioStreamWAV.new()
stream.set("format", AudioStreamWAV.FORMAT_16_BITS)
stream.mix_rate = SAMPLE_RATE
stream.stereo = false
stream.data = pcm
return stream

View file

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

View file

@ -25,30 +25,27 @@ const DEFAULT_PROFILE: Dictionary = {
"process/hdr_clamp_exposure": false, "process/hdr_clamp_exposure": false,
"process/size_limit": 0, "process/size_limit": 0,
"detect_3d/compress_to": 0, "detect_3d/compress_to": 0,
"import_profile": "managed_default",
}, },
} }
const CUSTOM_PROFILE_KEY := "custom" var apply_changes: bool = false
var verbose: bool = false
var apply_changes := false
var verbose := false
var roots: PackedStringArray = PackedStringArray() var roots: PackedStringArray = PackedStringArray()
var user_root_overrides := false var should_exit: bool = false
var should_exit := false
var skipped_custom := 0 var errors: int = 0
var skipped_non_texture := 0 var skipped_non_texture: int = 0
var skipped_unreadable := 0 var updated: int = 0
var updated := 0 var unchanged: int = 0
var unchanged := 0
func _initialize() -> void: func _initialize() -> void:
_parse_args() _parse_args()
if should_exit: if should_exit:
return return
if roots.is_empty() and not user_root_overrides: if roots.is_empty():
roots.append("res://") printerr("At least one --root path is required.")
quit(2)
return
for root: String in roots: for root: String in roots:
var normalized_root := root var normalized_root := root
@ -63,7 +60,7 @@ func _initialize() -> void:
_scan_directory(normalized_root) _scan_directory(normalized_root)
print_summary() print_summary()
quit(0) quit(1 if errors > 0 else 0)
func _parse_args() -> void: func _parse_args() -> void:
@ -89,25 +86,23 @@ func _parse_args() -> void:
quit(1) quit(1)
return return
i += 1 i += 1
roots.append(args[i]) roots.append(args[i])
i += 1 i += 1
user_root_overrides = true elif arg.begins_with("--root="):
elif arg.begins_with("--root="): roots.append(arg.trim_prefix("--root="))
roots.append(arg.trim_prefix("--root=")) i += 1
i += 1
user_root_overrides = true
else: else:
if arg.begins_with("-"): printerr("Unknown argument: ", arg)
_print("Ignoring unknown option: %s" % arg) should_exit = true
i += 1 quit(2)
continue return
func _scan_directory(path: String) -> void: func _scan_directory(path: String) -> void:
var dir := DirAccess.open(path) var dir := DirAccess.open(path)
if dir == null: if dir == null:
printerr("Failed to open root: ", path) printerr("Failed to open root: ", path)
skipped_non_texture += 1 errors += 1
return return
dir.list_dir_begin() dir.list_dir_begin()
@ -136,7 +131,7 @@ func _handle_import_file(import_path: String) -> void:
var err := cfg.load(import_path) var err := cfg.load(import_path)
if err != OK: if err != OK:
printerr("Could not load import settings: ", import_path, " (", err, ")") printerr("Could not load import settings: ", import_path, " (", err, ")")
skipped_unreadable += 1 errors += 1
return return
if not cfg.has_section("remap") or not cfg.has_section_key("remap", "importer"): if not cfg.has_section("remap") or not cfg.has_section_key("remap", "importer"):
@ -147,15 +142,6 @@ func _handle_import_file(import_path: String) -> void:
skipped_non_texture += 1 skipped_non_texture += 1
return return
var current_profile := ""
if cfg.has_section("params") and cfg.has_section_key("params", "import_profile"):
current_profile = str(cfg.get_value("params", "import_profile"))
if current_profile == CUSTOM_PROFILE_KEY:
skipped_custom += 1
if verbose:
_print("Skipping custom profile: %s" % import_path)
return
var changed := false var changed := false
for section_name: String in DEFAULT_PROFILE.keys(): for section_name: String in DEFAULT_PROFILE.keys():
var section_data: Dictionary = DEFAULT_PROFILE[section_name] var section_data: Dictionary = DEFAULT_PROFILE[section_name]
@ -178,7 +164,7 @@ func _handle_import_file(import_path: String) -> void:
err = cfg.save(import_path) err = cfg.save(import_path)
if err != OK: if err != OK:
printerr("Could not save import settings: ", import_path, " (", err, ")") printerr("Could not save import settings: ", import_path, " (", err, ")")
skipped_unreadable += 1 errors += 1
return return
if verbose: if verbose:
_print("Updated: %s" % import_path) _print("Updated: %s" % import_path)
@ -190,11 +176,18 @@ func _handle_import_file(import_path: String) -> void:
func print_summary() -> void: func print_summary() -> void:
if apply_changes: if apply_changes:
_print("Texture import normalization complete: updated=%d unchanged=%d skipped_custom=%d skipped_non_texture=%d skipped_unreadable=%d" % [updated, unchanged, skipped_custom, skipped_non_texture, skipped_unreadable]) _print(
"Texture import normalization complete: "
+ "updated=%d unchanged=%d skipped_non_texture=%d errors=%d"
% [updated, unchanged, skipped_non_texture, errors]
)
else: else:
_print("Dry-run complete: would-update=%d unchanged=%d skipped_custom=%d skipped_non_texture=%d skipped_unreadable=%d" % [updated, unchanged, skipped_custom, skipped_non_texture, skipped_unreadable]) _print(
"Dry-run complete: "
+ "would-update=%d unchanged=%d skipped_non_texture=%d errors=%d"
% [updated, unchanged, skipped_non_texture, errors]
)
_print("Pass --apply to write normalized settings into .import files.") _print("Pass --apply to write normalized settings into .import files.")
_print("Set [params] import_profile = \"custom\" to opt a texture out of default normalization.")
func _print(msg: String) -> void: func _print(msg: String) -> void:
@ -207,6 +200,5 @@ func _print_usage() -> void:
print() print()
print("Defaults:") print("Defaults:")
print("- Dry-run only unless --apply is provided") print("- Dry-run only unless --apply is provided")
print("- Scans all .import files under each root") print("- Requires at least one explicit --root path")
print("- Skips .godot") print("- Skips .godot")
print("- Skips textures marked with [params] import_profile = \"custom\"")

View file

@ -5,15 +5,26 @@ set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
readonly GODOT_BIN="${GODOT_BIN:-godot}" readonly GODOT_BIN="${GODOT_BIN:-godot}"
readonly ROOT_PATH="${1:-res://}"
if [[ $# -ne 1 ]]; then
echo "usage: scripts/normalize_texture_profiles.sh res://path/to/textures" >&2
exit 2
fi
readonly ROOT_PATH="$1"
readonly WORK_ROOT="$(mktemp -d -t netfishing-godot-data.XXXXXX)" readonly WORK_ROOT="$(mktemp -d -t netfishing-godot-data.XXXXXX)"
trap 'rm -rf -- "${WORK_ROOT}"' EXIT trap 'rm -rf -- "${WORK_ROOT}"' EXIT
cd "${PROJECT_ROOT}" cd "${PROJECT_ROOT}"
XDG_DATA_HOME="${WORK_ROOT}/data" \ export XDG_DATA_HOME="${WORK_ROOT}/data"
XDG_CONFIG_HOME="${WORK_ROOT}/config" \ export XDG_CONFIG_HOME="${WORK_ROOT}/config"
XDG_CACHE_HOME="${WORK_ROOT}/cache" \ export XDG_CACHE_HOME="${WORK_ROOT}/cache"
${GODOT_BIN} --headless --path . \
--script scripts/normalize_texture_imports.gd -- --apply --root "${ROOT_PATH}" "${GODOT_BIN}" --headless --path . \
--script scripts/normalize_texture_imports.gd -- --apply --root "${ROOT_PATH}"
# Synchronize Godot-generated remap and destination metadata with the updated
# import parameters before the caller reviews or commits the sidecars.
"${GODOT_BIN}" --headless --path . --import

View file

@ -18,7 +18,7 @@ readonly -a QUICK_TESTS=(
"tests/player_experience_validation.gd" "tests/player_experience_validation.gd"
"tests/shoreline_ambience_validation.gd" "tests/shoreline_ambience_validation.gd"
"tests/surface_drawing_validation.gd" "tests/surface_drawing_validation.gd"
"tests/terrain_projection_validation.gd" "tests/terrain_blender_material_validation.gd"
"tests/world_time_validation.gd" "tests/world_time_validation.gd"
"tests/world_weather_validation.gd" "tests/world_weather_validation.gd"
) )

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
ANDROID_HOME="${ANDROID_HOME:-/home/sol6_vi/Android/Sdk}" ANDROID_HOME="${ANDROID_HOME:-${HOME}/Android/Sdk}"
JAVA_HOME="${JAVA_HOME:-}" JAVA_HOME="${JAVA_HOME:-}"
INSTALL=0 INSTALL=0
AUTO=0 AUTO=0
@ -55,6 +55,9 @@ done
PROJECT_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" PROJECT_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
GODOT="${GODOT_BIN:-godot}" GODOT="${GODOT_BIN:-godot}"
PROJECT_VERSION="$(
sed -n 's/^config\/version="\([^"]*\)"/\1/p' "${PROJECT_ROOT}/project.godot"
)"
MISSING=0 MISSING=0
print_status() { printf '%s %s\n' "[$1]" "$2"; } print_status() { printf '%s %s\n' "[$1]" "$2"; }
@ -71,7 +74,7 @@ check_dir() {
check_file() { check_file() {
local path="$1" label="$2" local path="$1" label="$2"
if [[ -x "$path" || -f "$path" ]]; then if [[ -x "$path" ]]; then
print_status OK "${label}: ${path}" print_status OK "${label}: ${path}"
else else
print_status ERR "Missing ${label}: ${path}" print_status ERR "Missing ${label}: ${path}"
@ -113,6 +116,8 @@ resolve_java_home() {
check_java() { check_java() {
if resolve_java_home && [[ -x "$JAVA_HOME/bin/java" ]]; then if resolve_java_home && [[ -x "$JAVA_HOME/bin/java" ]]; then
print_status OK "Java binary: $JAVA_HOME/bin/java" print_status OK "Java binary: $JAVA_HOME/bin/java"
check_file "$JAVA_HOME/bin/javac" "javac binary"
check_file "$JAVA_HOME/bin/keytool" "keytool binary"
"$JAVA_HOME/bin/java" -version 2>&1 | sed -n '1,2p' "$JAVA_HOME/bin/java" -version 2>&1 | sed -n '1,2p'
local java_ver local java_ver
java_ver=$("$JAVA_HOME/bin/java" -version 2>&1 | sed -n '1p') java_ver=$("$JAVA_HOME/bin/java" -version 2>&1 | sed -n '1p')
@ -161,18 +166,13 @@ check_sdk_paths() {
check_dir "$ANDROID_HOME/platform-tools" "platform-tools" check_dir "$ANDROID_HOME/platform-tools" "platform-tools"
check_file "$ANDROID_HOME/platform-tools/adb" "adb binary" check_file "$ANDROID_HOME/platform-tools/adb" "adb binary"
if [[ -d "$ANDROID_HOME/build-tools" ]]; then if [[ -d "$ANDROID_HOME/build-tools/$BUILDTOOLS_VERSION" ]]; then
local bt print_status OK "build-tools ${BUILDTOOLS_VERSION}: present"
bt="$(ls -1v "$ANDROID_HOME/build-tools" 2>/dev/null | tail -n 1 || true)" check_file \
if [[ -n "$bt" ]]; then "$ANDROID_HOME/build-tools/$BUILDTOOLS_VERSION/apksigner" \
print_status OK "build-tools versions: $(ls -1 "$ANDROID_HOME/build-tools" | tr '\n' ' ')" "apksigner"
check_file "$ANDROID_HOME/build-tools/$bt/apksigner" "apksigner"
else
print_status ERR "build-tools directory exists but is empty"
MISSING=$((MISSING+1))
fi
else else
print_status ERR "Missing build-tools root: $ANDROID_HOME/build-tools" print_status ERR "Missing build-tools version: ${BUILDTOOLS_VERSION}"
MISSING=$((MISSING+1)) MISSING=$((MISSING+1))
fi fi
@ -198,6 +198,7 @@ check_sdk_paths() {
install_jdk_hint() { install_jdk_hint() {
local pm=0 local pm=0
local -a elevate=()
echo "Attempting automatic JDK install..." echo "Attempting automatic JDK install..."
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
pm="apt-get" pm="apt-get"
@ -212,19 +213,18 @@ install_jdk_hint() {
fi fi
if [[ $EUID -ne 0 ]]; then if [[ $EUID -ne 0 ]]; then
local sudo_cmd=(sudo)
if ! command -v sudo >/dev/null 2>&1; then if ! command -v sudo >/dev/null 2>&1; then
echo "sudo not found; please run with root privileges for package install." echo "sudo not found; please run with root privileges for package install."
return 1 return 1
fi fi
elevate=(sudo)
fi fi
if [[ "$pm" == "apt-get" ]]; then if [[ "$pm" == "apt-get" ]]; then
echo "apt-get install -y openjdk-17-jdk" echo "apt-get install -y openjdk-17-jdk"
if [[ $AUTO -eq 1 ]]; then if [[ $AUTO -eq 1 ]]; then
if command -v sudo >/dev/null 2>&1; then "${elevate[@]}" apt-get update
sudo apt-get update && sudo apt-get install -y openjdk-17-jdk "${elevate[@]}" apt-get install -y openjdk-17-jdk
fi
else else
echo "Run as manual step first: sudo apt-get update && sudo apt-get install -y openjdk-17-jdk" echo "Run as manual step first: sudo apt-get update && sudo apt-get install -y openjdk-17-jdk"
return 1 return 1
@ -232,14 +232,14 @@ install_jdk_hint() {
elif [[ "$pm" == "dnf" ]]; then elif [[ "$pm" == "dnf" ]]; then
echo "dnf install -y java-17-openjdk-devel" echo "dnf install -y java-17-openjdk-devel"
if [[ $AUTO -eq 1 ]]; then if [[ $AUTO -eq 1 ]]; then
sudo dnf install -y java-17-openjdk-devel "${elevate[@]}" dnf install -y java-17-openjdk-devel
else else
return 1 return 1
fi fi
else else
echo "pacman -S --noconfirm jdk17-openjdk" echo "pacman -S --noconfirm jdk17-openjdk"
if [[ $AUTO -eq 1 ]]; then if [[ $AUTO -eq 1 ]]; then
sudo pacman -S --noconfirm jdk17-openjdk "${elevate[@]}" pacman -S --noconfirm jdk17-openjdk
else else
return 1 return 1
fi fi
@ -305,7 +305,7 @@ install_sdk_components() {
echo "Cannot install SDK packages without sdkmanager." echo "Cannot install SDK packages without sdkmanager."
return 1 return 1
fi fi
"$sdkmanager_path" --sdk_root="${ANDROID_HOME}" --list | head -n 40 | sed -n '1,40p' "$sdkmanager_path" --sdk_root="${ANDROID_HOME}" --list | sed -n '1,40p'
echo "Installing required SDK components: platform-tools, platform-android-${SDK_VERSION}, build-tools-${BUILDTOOLS_VERSION}" echo "Installing required SDK components: platform-tools, platform-android-${SDK_VERSION}, build-tools-${BUILDTOOLS_VERSION}"
if [[ $AUTO -eq 1 ]]; then if [[ $AUTO -eq 1 ]]; then
yes | "$sdkmanager_path" --sdk_root="${ANDROID_HOME}" --install "platform-tools" "platforms;android-${SDK_VERSION}" "build-tools;${BUILDTOOLS_VERSION}" yes | "$sdkmanager_path" --sdk_root="${ANDROID_HOME}" --install "platform-tools" "platforms;android-${SDK_VERSION}" "build-tools;${BUILDTOOLS_VERSION}"
@ -336,7 +336,7 @@ check_editor_settings() {
run_export_hint() { run_export_hint() {
echo echo
echo "Example export commands:" echo "Example export commands:"
echo " ${GODOT} --headless --path \"$PROJECT_ROOT\" --export-release \"Android\" builds/v0.5.1-prealpha/android/NETFISHING.apk" echo " ${GODOT} --headless --path \"$PROJECT_ROOT\" --export-release \"Android\" builds/${PROJECT_VERSION}/android/NETfishing.apk"
echo " ${GODOT} --headless --path \"$PROJECT_ROOT\" --export-debug \"Android\" builds/android/NETfishing-debug.apk" echo " ${GODOT} --headless --path \"$PROJECT_ROOT\" --export-debug \"Android\" builds/android/NETfishing-debug.apk"
} }
@ -354,7 +354,12 @@ fi
if [[ $INSTALL -eq 1 ]]; then if [[ $INSTALL -eq 1 ]]; then
echo echo
print_status INFO "Attempting dependency install path..." print_status INFO "Attempting dependency install path..."
if [[ -z "$JAVA_HOME" || ! -x "$JAVA_HOME/bin/java" ]]; then if [[ \
-z "$JAVA_HOME" \
|| ! -x "$JAVA_HOME/bin/java" \
|| ! -x "$JAVA_HOME/bin/javac" \
|| ! -x "$JAVA_HOME/bin/keytool" \
]]; then
install_jdk_hint || true install_jdk_hint || true
fi fi
@ -377,11 +382,11 @@ if [[ $INSTALL -eq 1 ]]; then
fi fi
print_status ERR "Dependency check failed with ${MISSING} issue(s)." print_status ERR "Dependency check failed with ${MISSING} issue(s)."
cat <<'TIPS' cat <<TIPS
Suggested manual recovery: Suggested manual recovery:
1) Install OpenJDK 17 JDK. 1) Install OpenJDK 17 JDK.
2) Install Android SDK command line tools and run: 2) Install Android SDK command line tools and run:
sdkmanager --sdk_root="${ANDROID_HOME}" --install "platform-tools" "platforms;android-33" "build-tools;34.0.0" sdkmanager --sdk_root="${ANDROID_HOME}" --install "platform-tools" "platforms;android-${SDK_VERSION}" "build-tools;${BUILDTOOLS_VERSION}"
3) Set JAVA_HOME and ANDROID_HOME in your shell and Godot Exporter settings. 3) Set JAVA_HOME and ANDROID_HOME in your shell and Godot Exporter settings.
4) Re-run this script without --install (verification), then re-run Godot Android export. 4) Re-run this script without --install (verification), then re-run Godot Android export.
TIPS TIPS

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dvajjy4bwadio"
path="res://.godot/imported/a.wav-9ab4b1a9a555e6fcf07951f399769b30.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/a.wav"
dest_files=["res://.godot/imported/a.wav-9ab4b1a9a555e6fcf07951f399769b30.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cs30tdyuo6wtc"
path="res://.godot/imported/b.wav-eed860af2033bda258bd172899061b45.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/b.wav"
dest_files=["res://.godot/imported/b.wav-eed860af2033bda258bd172899061b45.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://2inox7x31xam"
path="res://.godot/imported/c.wav-61dc49533634805d70c71d509b63e37b.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/c.wav"
dest_files=["res://.godot/imported/c.wav-61dc49533634805d70c71d509b63e37b.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://yvha8eukw6l7"
path="res://.godot/imported/d.wav-910bc02bc42bdb1b37561959a317c28a.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/d.wav"
dest_files=["res://.godot/imported/d.wav-910bc02bc42bdb1b37561959a317c28a.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://8ujopf8aqs83"
path="res://.godot/imported/e.wav-7da5f8fcd017bf931de2bd49dc00f652.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/e.wav"
dest_files=["res://.godot/imported/e.wav-7da5f8fcd017bf931de2bd49dc00f652.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://c0gv4cphnfcxf"
path="res://.godot/imported/f.wav-8eb31f2de6240647a0d6d2520aadcd6a.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/f.wav"
dest_files=["res://.godot/imported/f.wav-8eb31f2de6240647a0d6d2520aadcd6a.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bw0pod4tmexm0"
path="res://.godot/imported/fallback.wav-7a40516b188f10028ac39bdc24fb52e5.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/fallback.wav"
dest_files=["res://.godot/imported/fallback.wav-7a40516b188f10028ac39bdc24fb52e5.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://c7706p0fc8y7c"
path="res://.godot/imported/g.wav-45b2cbd9653a31477e3f00c9432d560c.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/g.wav"
dest_files=["res://.godot/imported/g.wav-45b2cbd9653a31477e3f00c9432d560c.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cl66wka105xmo"
path="res://.godot/imported/h.wav-2da53a6638eebb75f0867eaf75dc24ba.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/h.wav"
dest_files=["res://.godot/imported/h.wav-2da53a6638eebb75f0867eaf75dc24ba.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://jicgdbnyt6bm"
path="res://.godot/imported/i.wav-1e8d7f13980a6f2f61d73f65a2efc91c.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/i.wav"
dest_files=["res://.godot/imported/i.wav-1e8d7f13980a6f2f61d73f65a2efc91c.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://cji2evr0u3m6c"
path="res://.godot/imported/j.wav-2f355374ffcea77f8dc630ee45879444.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/j.wav"
dest_files=["res://.godot/imported/j.wav-2f355374ffcea77f8dc630ee45879444.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://eypaqf1iaq5"
path="res://.godot/imported/k.wav-b85d045dc022b9055c25a650b2e3af2a.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/k.wav"
dest_files=["res://.godot/imported/k.wav-b85d045dc022b9055c25a650b2e3af2a.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://bixgnaq4idkhj"
path="res://.godot/imported/l.wav-b21006c3b048e0c797d6baf19ab0f663.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/l.wav"
dest_files=["res://.godot/imported/l.wav-b21006c3b048e0c797d6baf19ab0f663.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dan4o17oreugv"
path="res://.godot/imported/m.wav-aa394c4b1d5309d859ce73a368754264.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/m.wav"
dest_files=["res://.godot/imported/m.wav-aa394c4b1d5309d859ce73a368754264.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View file

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dxup6pw80km61"
path="res://.godot/imported/n.wav-84e9a6fa6664222868d647187a09f374.sample"
[deps]
source_file="res://sound/dialogue/animalese/placeholder/n.wav"
dest_files=["res://.godot/imported/n.wav-84e9a6fa6664222868d647187a09f374.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

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