Compare commits
14 commits
v0.19.0-al
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 014c064fae | |||
| 3be79fa3fc | |||
| 6578d3e870 | |||
| ee992d9f0c | |||
| 9b96a3bf53 | |||
| 826f62c352 | |||
| b48cb73f3a | |||
| d2dce7f900 | |||
| 983e4d0ec3 | |||
| fe099f47dd | |||
| 81decf2b71 | |||
| 4575830857 | |||
| ae802493a0 | |||
| f910e6bdbb |
90 changed files with 8664 additions and 261 deletions
|
|
@ -22,9 +22,9 @@ Discord: https://discord.gg/5gP22447kc
|
||||||
|
|
||||||
Matrix: https://matrix.to/#/#straywild:matrix.makearmy.io
|
Matrix: https://matrix.to/#/#straywild:matrix.makearmy.io
|
||||||
|
|
||||||
Repo: https://forge.makearmy.io/woofmeow/netfishing
|
Repo: https://forge.makearmy.io/woofmeow/straywild
|
||||||
|
|
||||||
Releases: https://forge.makearmy.io/woofmeow/netfishing/releases
|
Releases: https://forge.makearmy.io/woofmeow/straywild/releases
|
||||||
|
|
||||||
|
|
||||||
## Installing release builds
|
## Installing release builds
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,10 @@ environment and UI artwork, item artwork, finished fish and creature artwork,
|
||||||
and character customization pattern and channel-map artwork. Voyager's
|
and character customization pattern and channel-map artwork. Voyager's
|
||||||
incorporated original work includes 3D assets, the provisional fish
|
incorporated original work includes 3D assets, the provisional fish
|
||||||
placeholder artwork, the title and dusk music identified above, and the robot
|
placeholder artwork, the title and dusk music identified above, and the robot
|
||||||
animalese tones. Specific contributor exceptions are recorded below and in the
|
animalese tones. The current starter motorcoach exterior was supplied by a
|
||||||
in-game credits.
|
contributing artist who released its copyright to the project; the original
|
||||||
|
and cleaned source files are retained in the owners' source-work archive.
|
||||||
|
Specific contributor exceptions are recorded below and in the in-game credits.
|
||||||
|
|
||||||
These credits intentionally describe work by creator and contribution family
|
These credits intentionally describe work by creator and contribution family
|
||||||
instead of maintaining a second file-by-file asset catalog. Runtime resources
|
instead of maintaining a second file-by-file asset catalog. Runtime resources
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
straywild
|
straywild
|
||||||
v0.19.0-alpha
|
v0.20.2-alpha
|
||||||
Alpha 0.19.0
|
Alpha 0.20.2
|
||||||
|
|
||||||
Thank you for trying this early private playtest.
|
Thank you for supporting this private testing release.
|
||||||
|
|
||||||
straywild is an early multiplayer fishing game. This alpha includes local
|
straywild is an early multiplayer fishing game. This alpha includes local
|
||||||
progression, player-hosted and dedicated multiplayer sessions, public room
|
progression, player-hosted and dedicated multiplayer sessions, public room
|
||||||
|
|
@ -83,6 +83,8 @@ FEATURES TO TRY
|
||||||
- Fishing, net gathering, and beach digging
|
- Fishing, net gathering, and beach digging
|
||||||
- Equipment and consumable use from the hotbar
|
- Equipment and consumable use from the hotbar
|
||||||
- Fishing Shop sell tray, wallet updates, and persistent upgrades
|
- Fishing Shop sell tray, wallet updates, and persistent upgrades
|
||||||
|
- Portable RV homes, multiplayer visits, RV storage, and the early decor
|
||||||
|
purchasing and placement prototype
|
||||||
- Seeded generated worlds and the authored starter-island layout
|
- Seeded generated worlds and the authored starter-island layout
|
||||||
- Generated beaches, ponds, lakes, complete rivers, denser forests, varied
|
- Generated beaches, ponds, lakes, complete rivers, denser forests, varied
|
||||||
elevation, and closed coastal cliffs
|
elevation, and closed coastal cliffs
|
||||||
|
|
@ -117,5 +119,6 @@ KNOWN ALPHA LIMITATIONS
|
||||||
networks may prevent some players from joining.
|
networks may prevent some players from joining.
|
||||||
- Multiplayer, dedicated-server deployment, UI, and balance are still alpha.
|
- Multiplayer, dedicated-server deployment, UI, and balance are still alpha.
|
||||||
- Some fish and character artwork remains temporary.
|
- Some fish and character artwork remains temporary.
|
||||||
|
- RV exteriors and decor use temporary prototype geometry and presentation.
|
||||||
- There is no installer or automatic updater.
|
- There is no installer or automatic updater.
|
||||||
- Windows builds are unsigned.
|
- Windows builds are unsigned.
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@ for local README files.
|
||||||
|
|
||||||
## Related repositories
|
## Related repositories
|
||||||
|
|
||||||
- [Discovery service API](https://forge.makearmy.io/woofmeow/netfishing-discovery-server/src/branch/main/docs/API.md)
|
- [Discovery service API](https://forge.makearmy.io/woofmeow/straywild-discovery-server/src/branch/main/docs/API.md)
|
||||||
stays with the service implementation and defines its versioned HTTP
|
stays with the service implementation and defines its versioned HTTP
|
||||||
contract.
|
contract.
|
||||||
- [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/netfishing-dedicated-server/src/branch/main/README.md)
|
- [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/straywild-dedicated-server/src/branch/main/README.md)
|
||||||
stays with its installer and update scripts. It consumes this repository's
|
stays with its installer and update scripts. It consumes this repository's
|
||||||
attribution and legal notices from the exact pinned game commit instead of
|
attribution and legal notices from the exact pinned game commit instead of
|
||||||
maintaining copies.
|
maintaining copies.
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.19.0-alpha/windows-x86_64/straywild.exe"
|
export_path="builds/v0.20.2-alpha/windows-x86_64/straywild.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -35,11 +35,11 @@ application/modify_resources=true
|
||||||
application/icon="res://art/exported/system_icons/straywild.ico"
|
application/icon="res://art/exported/system_icons/straywild.ico"
|
||||||
application/console_wrapper_icon=""
|
application/console_wrapper_icon=""
|
||||||
application/icon_interpolation=0
|
application/icon_interpolation=0
|
||||||
application/file_version="0.19.0.0"
|
application/file_version="0.20.2.0"
|
||||||
application/product_version="0.19.0.0"
|
application/product_version="0.20.2.0"
|
||||||
application/company_name="Woofmeow"
|
application/company_name="Woofmeow"
|
||||||
application/product_name="straywild"
|
application/product_name="straywild"
|
||||||
application/file_description="straywild v0.19.0-alpha"
|
application/file_description="straywild v0.20.2-alpha"
|
||||||
application/copyright="Copyright © 2026 Woofmeow"
|
application/copyright="Copyright © 2026 Woofmeow"
|
||||||
application/trademarks="straywild and Woofmeow branding is reserved; see TRADEMARKS.md"
|
application/trademarks="straywild and Woofmeow branding is reserved; see TRADEMARKS.md"
|
||||||
application/export_angle=0
|
application/export_angle=0
|
||||||
|
|
@ -62,7 +62,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.19.0-alpha/linux-arm64/straywild.arm64"
|
export_path="builds/v0.20.2-alpha/linux-arm64/straywild.arm64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -92,7 +92,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.19.0-alpha/macos/straywild.zip"
|
export_path="builds/v0.20.2-alpha/macos/straywild.zip"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -108,8 +108,8 @@ custom_template/release=""
|
||||||
application/bundle_identifier="io.woofmeow.netfishing"
|
application/bundle_identifier="io.woofmeow.netfishing"
|
||||||
application/icon="res://art/exported/system_icons/straywild_1024.png"
|
application/icon="res://art/exported/system_icons/straywild_1024.png"
|
||||||
application/icon_interpolation=0
|
application/icon_interpolation=0
|
||||||
application/short_version="0.19.0"
|
application/short_version="0.20.2"
|
||||||
application/version="0.19.0"
|
application/version="0.20.2"
|
||||||
application/architecture="universal"
|
application/architecture="universal"
|
||||||
codesign/enable=false
|
codesign/enable=false
|
||||||
notarization/enable=false
|
notarization/enable=false
|
||||||
|
|
@ -125,7 +125,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*,tests/*"
|
exclude_filter="builds/*,scripts/*,tests/*"
|
||||||
export_path="builds/v0.19.0-alpha/server-linux-x86_64/straywildServer.x86_64"
|
export_path="builds/v0.20.2-alpha/server-linux-x86_64/straywildServer.x86_64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -155,7 +155,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.19.0-alpha/android/straywild.apk"
|
export_path="builds/v0.20.2-alpha/android/straywild.apk"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
@ -176,8 +176,8 @@ architectures/armeabi-v7a=false
|
||||||
architectures/arm64-v8a=true
|
architectures/arm64-v8a=true
|
||||||
architectures/x86=false
|
architectures/x86=false
|
||||||
architectures/x86_64=false
|
architectures/x86_64=false
|
||||||
version/code=190000
|
version/code=200200
|
||||||
version/name="v0.19.0-alpha"
|
version/name="v0.20.2-alpha"
|
||||||
package/unique_name="io.woofmeow.netfishing"
|
package/unique_name="io.woofmeow.netfishing"
|
||||||
package/name="straywild"
|
package/name="straywild"
|
||||||
package/signed=true
|
package/signed=true
|
||||||
|
|
@ -214,7 +214,7 @@ custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||||
exclude_filter="builds/*,scripts/*"
|
exclude_filter="builds/*,scripts/*"
|
||||||
export_path="builds/v0.19.0-alpha/linux-x86_64/straywild.x86_64"
|
export_path="builds/v0.20.2-alpha/linux-x86_64/straywild.x86_64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
encryption_include_filters=""
|
encryption_include_filters=""
|
||||||
encryption_exclude_filters=""
|
encryption_exclude_filters=""
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,6 @@ const FishingSurfaceSampleType = preload(
|
||||||
const FishingSurfaceResolverType = preload(
|
const FishingSurfaceResolverType = preload(
|
||||||
"res://fishing/fishing_surface_resolver.gd"
|
"res://fishing/fishing_surface_resolver.gd"
|
||||||
)
|
)
|
||||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
|
||||||
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
|
||||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||||
const WorldWeatherServiceType = preload(
|
const WorldWeatherServiceType = preload(
|
||||||
"res://world/world_weather_service.gd"
|
"res://world/world_weather_service.gd"
|
||||||
|
|
@ -134,6 +132,7 @@ const ALTERNATE_REEL_ACTION: StringName = &"reel_alternate"
|
||||||
var state: FishingState = FishingState.READY
|
var state: FishingState = FishingState.READY
|
||||||
var _external_input_blocked: bool = false
|
var _external_input_blocked: bool = false
|
||||||
var _gameplay_input_enabled: bool = false
|
var _gameplay_input_enabled: bool = false
|
||||||
|
var _fishing_environment_enabled: bool = true
|
||||||
var _local_menu_input_owners: Dictionary[StringName, bool] = {}
|
var _local_menu_input_owners: Dictionary[StringName, bool] = {}
|
||||||
var _local_player: PlayerType
|
var _local_player: PlayerType
|
||||||
var _local_inventory: FishInventoryType
|
var _local_inventory: FishInventoryType
|
||||||
|
|
@ -150,6 +149,7 @@ var _network_session: NetworkSessionType
|
||||||
var _network_fishing: NetworkFishingServiceType
|
var _network_fishing: NetworkFishingServiceType
|
||||||
var _world_time: WorldTimeServiceType
|
var _world_time: WorldTimeServiceType
|
||||||
var _world_weather: WorldWeatherServiceType
|
var _world_weather: WorldWeatherServiceType
|
||||||
|
var _fishable_water_regions_resolver := Callable()
|
||||||
var _active_player: PlayerType
|
var _active_player: PlayerType
|
||||||
var _state_time_remaining: float = 0.0
|
var _state_time_remaining: float = 0.0
|
||||||
var _cast_charge: float = 0.0
|
var _cast_charge: float = 0.0
|
||||||
|
|
@ -222,6 +222,7 @@ func setup(
|
||||||
network_fishing: NetworkFishingServiceType = null,
|
network_fishing: NetworkFishingServiceType = null,
|
||||||
world_time: WorldTimeServiceType = null,
|
world_time: WorldTimeServiceType = null,
|
||||||
world_weather: WorldWeatherServiceType = null,
|
world_weather: WorldWeatherServiceType = null,
|
||||||
|
fishable_water_regions_resolver: Callable = Callable(),
|
||||||
) -> void:
|
) -> void:
|
||||||
_local_player = local_player
|
_local_player = local_player
|
||||||
_local_inventory = local_inventory
|
_local_inventory = local_inventory
|
||||||
|
|
@ -242,6 +243,7 @@ func setup(
|
||||||
_network_fishing = network_fishing
|
_network_fishing = network_fishing
|
||||||
_world_time = world_time
|
_world_time = world_time
|
||||||
_world_weather = world_weather
|
_world_weather = world_weather
|
||||||
|
_fishable_water_regions_resolver = fishable_water_regions_resolver
|
||||||
if (
|
if (
|
||||||
_network_session != null
|
_network_session != null
|
||||||
and not _network_session.state_changed.is_connected(
|
and not _network_session.state_changed.is_connected(
|
||||||
|
|
@ -396,6 +398,22 @@ func set_gameplay_input_enabled(enabled: bool) -> void:
|
||||||
reset_transient_state()
|
reset_transient_state()
|
||||||
|
|
||||||
|
|
||||||
|
func set_fishing_environment_enabled(enabled: bool) -> void:
|
||||||
|
if _fishing_environment_enabled == enabled:
|
||||||
|
return
|
||||||
|
_fishing_environment_enabled = enabled
|
||||||
|
if enabled:
|
||||||
|
_new_cast_press_armed = not Input.is_action_pressed("fish_primary")
|
||||||
|
return
|
||||||
|
if _network_fishing != null and _network_fishing.has_local_attempt():
|
||||||
|
_network_fishing.cancel_local_attempt("")
|
||||||
|
reset_transient_state()
|
||||||
|
|
||||||
|
|
||||||
|
func is_fishing_environment_enabled() -> bool:
|
||||||
|
return _fishing_environment_enabled
|
||||||
|
|
||||||
|
|
||||||
func reset_transient_state(restore_movement: bool = true) -> void:
|
func reset_transient_state(restore_movement: bool = true) -> void:
|
||||||
var local_state_was_active: bool = (
|
var local_state_was_active: bool = (
|
||||||
state != FishingState.READY or _active_player != null
|
state != FishingState.READY or _active_player != null
|
||||||
|
|
@ -530,6 +548,7 @@ func _process(delta: float) -> void:
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if (
|
if (
|
||||||
not _gameplay_input_enabled
|
not _gameplay_input_enabled
|
||||||
|
or not _fishing_environment_enabled
|
||||||
or _external_input_blocked
|
or _external_input_blocked
|
||||||
or not _local_menu_input_owners.is_empty()
|
or not _local_menu_input_owners.is_empty()
|
||||||
):
|
):
|
||||||
|
|
@ -542,51 +561,23 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
alternate_reel and state == FishingState.FIGHTING
|
alternate_reel and state == FishingState.FIGHTING
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
# Fishing owns the primary action only after a rod starts a cast, or while
|
||||||
|
# completing an already-active fishing sequence. An empty hand, decor,
|
||||||
|
# gathering tools, and other items must remain available to their own world
|
||||||
|
# interaction controllers.
|
||||||
|
if state == FishingState.READY and not has_active_fishing_rod():
|
||||||
|
return
|
||||||
# A held keyboard key emits echo events. Treat the alternate reel exactly
|
# A held keyboard key emits echo events. Treat the alternate reel exactly
|
||||||
# like a mouse button: one barrier hit per physical press, while the normal
|
# like a mouse button: one barrier hit per physical press, while the normal
|
||||||
# held-input path continues driving free reeling between barriers.
|
# held-input path continues driving free reeling between barriers.
|
||||||
if event is InputEventKey and event.echo:
|
if event is InputEventKey and event.echo:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
var selected_item: ItemDataType = _get_active_item()
|
|
||||||
if (
|
|
||||||
state == FishingState.READY
|
|
||||||
and selected_item != null
|
|
||||||
and FishingShopStockType.is_gathering_tool(selected_item.item_id)
|
|
||||||
):
|
|
||||||
return
|
|
||||||
if event.is_pressed():
|
if event.is_pressed():
|
||||||
match state:
|
match state:
|
||||||
FishingState.READY:
|
FishingState.READY:
|
||||||
if not _new_cast_press_armed:
|
if not _new_cast_press_armed:
|
||||||
return
|
return
|
||||||
var active_item: ItemDataType = _get_active_item()
|
|
||||||
if (
|
|
||||||
active_item != null
|
|
||||||
and (
|
|
||||||
active_item.category == ItemDataType.Category.CONSUMABLE
|
|
||||||
or (
|
|
||||||
active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID
|
|
||||||
)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
if _network_item_use != null:
|
|
||||||
_network_item_use.request_use(active_item.item_id)
|
|
||||||
elif _item_effects.use_item(active_item, _local_bag):
|
|
||||||
status_changed.emit(_item_effects.get_feedback(
|
|
||||||
active_item.item_id
|
|
||||||
))
|
|
||||||
elif (
|
|
||||||
active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID
|
|
||||||
):
|
|
||||||
local_speech_requested.emit(
|
|
||||||
PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE
|
|
||||||
)
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if not has_active_fishing_rod():
|
|
||||||
get_viewport().set_input_as_handled()
|
|
||||||
return
|
|
||||||
if (
|
if (
|
||||||
_is_inventory_full()
|
_is_inventory_full()
|
||||||
):
|
):
|
||||||
|
|
@ -658,6 +649,34 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func try_use_active_non_fishing_item() -> bool:
|
||||||
|
if (
|
||||||
|
not _gameplay_input_enabled
|
||||||
|
or _external_input_blocked
|
||||||
|
or not _local_menu_input_owners.is_empty()
|
||||||
|
or state != FishingState.READY
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var active_item: ItemDataType = _get_active_item()
|
||||||
|
if (
|
||||||
|
active_item == null
|
||||||
|
or (
|
||||||
|
active_item.category != ItemDataType.Category.CONSUMABLE
|
||||||
|
and active_item.item_id != PlayerItemEffectsType.FISH_FINDER_ID
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
if _network_item_use != null:
|
||||||
|
_network_item_use.request_use(active_item.item_id)
|
||||||
|
elif _item_effects.use_item(active_item, _local_bag):
|
||||||
|
status_changed.emit(_item_effects.get_feedback(active_item.item_id))
|
||||||
|
elif active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID:
|
||||||
|
local_speech_requested.emit(
|
||||||
|
PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _is_reel_input_pressed() -> bool:
|
func _is_reel_input_pressed() -> bool:
|
||||||
return (
|
return (
|
||||||
Input.is_action_pressed("fish_primary")
|
Input.is_action_pressed("fish_primary")
|
||||||
|
|
@ -1664,6 +1683,47 @@ func build_network_context(
|
||||||
return _build_fishing_context(region)
|
return _build_fishing_context(region)
|
||||||
|
|
||||||
|
|
||||||
|
func is_species_available_now(fish: FishDataType) -> bool:
|
||||||
|
if fish == null or not fish.is_selectable():
|
||||||
|
return false
|
||||||
|
var season := (
|
||||||
|
_world_time.get_season()
|
||||||
|
if _world_time != null
|
||||||
|
else CalendarSeason.UNKNOWN
|
||||||
|
)
|
||||||
|
if not fish.is_available_in_season(season):
|
||||||
|
return false
|
||||||
|
if fish.collection_method != FishDataType.CollectionMethod.FISHING:
|
||||||
|
return true
|
||||||
|
for region: FishableWaterRegionType in _get_logbook_water_regions():
|
||||||
|
if (
|
||||||
|
region == null
|
||||||
|
or region.fish_pool == null
|
||||||
|
or region.fish_pool.get_fish_by_id(fish.id) == null
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var context := _build_fishing_context(region)
|
||||||
|
if not fish.is_allowed_in_water(context.water_type):
|
||||||
|
continue
|
||||||
|
if fish.availability == null or fish.availability.is_available(context):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _get_logbook_water_regions() -> Array[FishableWaterRegionType]:
|
||||||
|
var regions: Array[FishableWaterRegionType] = []
|
||||||
|
if not _fishable_water_regions_resolver.is_valid():
|
||||||
|
return regions
|
||||||
|
var resolved: Variant = _fishable_water_regions_resolver.call()
|
||||||
|
if resolved is not Array:
|
||||||
|
return regions
|
||||||
|
for value: Variant in resolved:
|
||||||
|
var region := value as FishableWaterRegionType
|
||||||
|
if region != null and is_instance_valid(region):
|
||||||
|
regions.append(region)
|
||||||
|
return regions
|
||||||
|
|
||||||
|
|
||||||
func _get_active_bait_tags() -> Array[StringName]:
|
func _get_active_bait_tags() -> Array[StringName]:
|
||||||
if _local_player == null or _item_catalog == null:
|
if _local_player == null or _item_catalog == null:
|
||||||
return []
|
return []
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,16 @@ func configure(
|
||||||
entity_id = configured_entity_id
|
entity_id = configured_entity_id
|
||||||
data = configured_data
|
data = configured_data
|
||||||
type_id = data.type_id if data != null else StringName()
|
type_id = data.type_id if data != null else StringName()
|
||||||
|
# Establish the authoritative transform before constructing transient visuals.
|
||||||
|
# Effects emitted before this point would begin at the default world origin.
|
||||||
|
apply_network_state(position, yaw, true)
|
||||||
if data != null and data.is_stationary_hotspot():
|
if data != null and data.is_stationary_hotspot():
|
||||||
_ensure_water_spurt_visual()
|
_ensure_water_spurt_visual()
|
||||||
_water_spurt_elapsed = (
|
_water_spurt_elapsed = (
|
||||||
float(abs(hash(entity_id)) % 1000) / 1000.0
|
float(abs(hash(entity_id)) % 1000) / 1000.0
|
||||||
* WATER_SPURT_INTERVAL_SECONDS
|
* WATER_SPURT_INTERVAL_SECONDS
|
||||||
)
|
)
|
||||||
|
_emit_water_spurt()
|
||||||
else:
|
else:
|
||||||
_ensure_visual()
|
_ensure_visual()
|
||||||
if data != null and data.catch_data != null and _sprite != null:
|
if data != null and data.catch_data != null and _sprite != null:
|
||||||
|
|
@ -50,7 +54,6 @@ func configure(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_sprite.rotation_degrees.x = data.sprite_tilt_degrees
|
_sprite.rotation_degrees.x = data.sprite_tilt_degrees
|
||||||
apply_network_state(position, yaw, true)
|
|
||||||
|
|
||||||
|
|
||||||
func apply_network_state(
|
func apply_network_state(
|
||||||
|
|
@ -147,7 +150,6 @@ func _ensure_water_spurt_visual() -> void:
|
||||||
hole.mesh = hole_mesh
|
hole.mesh = hole_mesh
|
||||||
hole.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
hole.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
_water_spurt_root.add_child(hole)
|
_water_spurt_root.add_child(hole)
|
||||||
_emit_water_spurt()
|
|
||||||
|
|
||||||
|
|
||||||
func _emit_water_spurt() -> void:
|
func _emit_water_spurt() -> void:
|
||||||
|
|
@ -159,6 +161,7 @@ func _emit_water_spurt() -> void:
|
||||||
particles.amount = 6
|
particles.amount = 6
|
||||||
particles.lifetime = 0.42
|
particles.lifetime = 0.42
|
||||||
particles.one_shot = true
|
particles.one_shot = true
|
||||||
|
particles.local_coords = true
|
||||||
particles.explosiveness = 1.0
|
particles.explosiveness = 1.0
|
||||||
particles.direction = Vector3.UP
|
particles.direction = Vector3.UP
|
||||||
particles.spread = 32.0
|
particles.spread = 32.0
|
||||||
|
|
|
||||||
BIN
homes/assets/starter_motorcoach.glb
Normal file
BIN
homes/assets/starter_motorcoach.glb
Normal file
Binary file not shown.
45
homes/assets/starter_motorcoach.glb.import
Normal file
45
homes/assets/starter_motorcoach.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bhph22caipcj6"
|
||||||
|
path="res://.godot/imported/starter_motorcoach.glb-153681ef1e622a556fba4df576dc6f84.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://homes/assets/starter_motorcoach.glb"
|
||||||
|
dest_files=["res://.godot/imported/starter_motorcoach.glb-153681ef1e622a556fba4df576dc6f84.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
59
homes/decor_catalog.gd
Normal file
59
homes/decor_catalog.gd
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
class_name DecorCatalog
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const BASIC_CUBE_ID: StringName = &"basic_cube"
|
||||||
|
const MAX_OWNED_PER_PRODUCT: int = 32
|
||||||
|
|
||||||
|
const _PRODUCTS: Dictionary = {
|
||||||
|
BASIC_CUBE_ID: {
|
||||||
|
"display_name": "basic cube",
|
||||||
|
"description": "a simple piece of decor for testing your RV layout.",
|
||||||
|
"price": 1,
|
||||||
|
"footprint": Vector2(1.0, 0.8),
|
||||||
|
"height": 0.8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func get_product_ids() -> Array[StringName]:
|
||||||
|
var ids: Array[StringName] = []
|
||||||
|
for value: Variant in _PRODUCTS.keys():
|
||||||
|
ids.append(StringName(str(value)))
|
||||||
|
ids.sort_custom(func(first: StringName, second: StringName) -> bool:
|
||||||
|
return String(first) < String(second)
|
||||||
|
)
|
||||||
|
return ids
|
||||||
|
|
||||||
|
|
||||||
|
static func has_product(product_id: StringName) -> bool:
|
||||||
|
return _PRODUCTS.has(product_id)
|
||||||
|
|
||||||
|
|
||||||
|
static func get_product(product_id: StringName) -> Dictionary:
|
||||||
|
var value: Variant = _PRODUCTS.get(product_id)
|
||||||
|
return (
|
||||||
|
(value as Dictionary).duplicate(true)
|
||||||
|
if typeof(value) == TYPE_DICTIONARY
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func get_display_name(product_id: StringName) -> String:
|
||||||
|
return str(get_product(product_id).get("display_name", product_id))
|
||||||
|
|
||||||
|
|
||||||
|
static func get_description(product_id: StringName) -> String:
|
||||||
|
return str(get_product(product_id).get("description", ""))
|
||||||
|
|
||||||
|
|
||||||
|
static func get_price(product_id: StringName) -> int:
|
||||||
|
return int(get_product(product_id).get("price", -1))
|
||||||
|
|
||||||
|
|
||||||
|
static func get_footprint(product_id: StringName) -> Vector2:
|
||||||
|
var value: Variant = get_product(product_id).get("footprint", Vector2.ZERO)
|
||||||
|
return value as Vector2 if value is Vector2 else Vector2.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
static func get_height(product_id: StringName) -> float:
|
||||||
|
return float(get_product(product_id).get("height", 0.0))
|
||||||
1
homes/decor_catalog.gd.uid
Normal file
1
homes/decor_catalog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bvgwmexas2gps
|
||||||
499
homes/home_decor_controller.gd
Normal file
499
homes/home_decor_controller.gd
Normal file
|
|
@ -0,0 +1,499 @@
|
||||||
|
class_name HomeDecorController
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||||
|
const DRAG_RESPONSE: float = 22.0
|
||||||
|
const DRAG_POSITION_EPSILON: float = 0.005
|
||||||
|
|
||||||
|
var _player: Player
|
||||||
|
var _hotbar: PlayerHotbar
|
||||||
|
var _bag: PlayerBag
|
||||||
|
var _home_state: PlayerHomeState
|
||||||
|
var _network_home: NetworkHomeService
|
||||||
|
var _home_world: HomeWorldService
|
||||||
|
var _fishing_spot: FishingSpot
|
||||||
|
var _toolbar: HomeDecorToolbar
|
||||||
|
var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID
|
||||||
|
var _selected_instance_id: String = ""
|
||||||
|
var _drag_instance_id: String = ""
|
||||||
|
var _drag_owner_fingerprint: String = ""
|
||||||
|
var _drag_pointer_offset: Vector3 = Vector3.ZERO
|
||||||
|
var _drag_original_position: Vector3 = Vector3.ZERO
|
||||||
|
var _drag_original_yaw: float = 0.0
|
||||||
|
var _drag_target_position: Vector3 = Vector3.ZERO
|
||||||
|
var _drag_target_yaw: float = 0.0
|
||||||
|
var _drag_target_valid: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
player: Player,
|
||||||
|
hotbar: PlayerHotbar,
|
||||||
|
bag: PlayerBag,
|
||||||
|
home_state: PlayerHomeState,
|
||||||
|
network_home: NetworkHomeService,
|
||||||
|
home_world: HomeWorldService,
|
||||||
|
fishing_spot: FishingSpot,
|
||||||
|
toolbar: HomeDecorToolbar,
|
||||||
|
) -> void:
|
||||||
|
_player = player
|
||||||
|
_hotbar = hotbar
|
||||||
|
_bag = bag
|
||||||
|
_home_state = home_state
|
||||||
|
_network_home = network_home
|
||||||
|
_home_world = home_world
|
||||||
|
_fishing_spot = fishing_spot
|
||||||
|
_toolbar = toolbar
|
||||||
|
if not _network_home.local_space_changed.is_connected(
|
||||||
|
_on_local_space_changed
|
||||||
|
):
|
||||||
|
_network_home.local_space_changed.connect(_on_local_space_changed)
|
||||||
|
if not _network_home.local_manifest_rejected.is_connected(
|
||||||
|
_on_local_manifest_rejected
|
||||||
|
):
|
||||||
|
_network_home.local_manifest_rejected.connect(
|
||||||
|
_on_local_manifest_rejected
|
||||||
|
)
|
||||||
|
if not _home_state.changed.is_connected(_on_home_changed):
|
||||||
|
_home_state.changed.connect(_on_home_changed)
|
||||||
|
if not _toolbar.placement_mode_requested.is_connected(
|
||||||
|
_on_placement_mode_requested
|
||||||
|
):
|
||||||
|
_toolbar.placement_mode_requested.connect(
|
||||||
|
_on_placement_mode_requested
|
||||||
|
)
|
||||||
|
_toolbar.rotate_requested.connect(_rotate_selected)
|
||||||
|
_toolbar.store_requested.connect(_store_selected)
|
||||||
|
_toolbar.cancel_requested.connect(_clear_selection)
|
||||||
|
_toolbar.set_mode(_mode)
|
||||||
|
_refresh_toolbar()
|
||||||
|
|
||||||
|
|
||||||
|
func begin_decorating() -> void:
|
||||||
|
_refresh_toolbar()
|
||||||
|
if _toolbar.visible:
|
||||||
|
_toolbar.report_status(
|
||||||
|
"use held decor to place it; click or drag placed decor"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if _drag_instance_id.is_empty():
|
||||||
|
return
|
||||||
|
var pointer_motion := event as InputEventMouseMotion
|
||||||
|
if pointer_motion != null:
|
||||||
|
_update_drag(pointer_motion.position)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
var pointer_button := event as InputEventMouseButton
|
||||||
|
if (
|
||||||
|
pointer_button != null
|
||||||
|
and pointer_button.button_index == MOUSE_BUTTON_LEFT
|
||||||
|
and not pointer_button.pressed
|
||||||
|
):
|
||||||
|
# A gameplay menu can take ownership between the press and release. Do
|
||||||
|
# not let that release finalize decor behind the menu; preserve the
|
||||||
|
# original transform instead.
|
||||||
|
if _can_decorate():
|
||||||
|
_finish_drag()
|
||||||
|
else:
|
||||||
|
_cancel_drag()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
var pointer_button := event as InputEventMouseButton
|
||||||
|
if (
|
||||||
|
pointer_button != null
|
||||||
|
and pointer_button.button_index == MOUSE_BUTTON_LEFT
|
||||||
|
):
|
||||||
|
if not pointer_button.pressed or not _can_decorate():
|
||||||
|
return
|
||||||
|
if _begin_drag(pointer_button.position):
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
var pointer_decor_id: StringName = _get_selected_decor_id()
|
||||||
|
if (
|
||||||
|
not pointer_decor_id.is_empty()
|
||||||
|
and _bag.get_quantity(pointer_decor_id) > 0
|
||||||
|
):
|
||||||
|
_place_carried_decor(pointer_decor_id)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
if not _can_decorate() or not event.is_action_pressed("fish_primary"):
|
||||||
|
return
|
||||||
|
if event is InputEventKey and event.echo:
|
||||||
|
return
|
||||||
|
var decor_id: StringName = _get_selected_decor_id()
|
||||||
|
if not decor_id.is_empty() and _bag.get_quantity(decor_id) > 0:
|
||||||
|
_place_carried_decor(decor_id)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if _drag_instance_id.is_empty():
|
||||||
|
return
|
||||||
|
# Menus and other gameplay input handoffs must cancel an active drag before
|
||||||
|
# evaluating a held/released pointer. This prevents a same-frame TAB press
|
||||||
|
# followed by left-button release from committing a move behind the UI.
|
||||||
|
if not _can_decorate():
|
||||||
|
_cancel_drag()
|
||||||
|
return
|
||||||
|
if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
|
||||||
|
_finish_drag()
|
||||||
|
return
|
||||||
|
if not _drag_target_valid:
|
||||||
|
return
|
||||||
|
var current: Dictionary = _home_world.get_decor_preview_transform(
|
||||||
|
_drag_owner_fingerprint, _drag_instance_id
|
||||||
|
)
|
||||||
|
if current.is_empty():
|
||||||
|
_cancel_drag()
|
||||||
|
return
|
||||||
|
var current_position: Vector3 = current["position"]
|
||||||
|
var current_yaw: float = float(current["yaw"])
|
||||||
|
var blend: float = 1.0 - exp(-DRAG_RESPONSE * delta)
|
||||||
|
_home_world.set_decor_preview_transform(
|
||||||
|
_drag_owner_fingerprint,
|
||||||
|
_drag_instance_id,
|
||||||
|
current_position.lerp(_drag_target_position, blend),
|
||||||
|
lerp_angle(current_yaw, _drag_target_yaw, blend),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _begin_drag(screen_position: Vector2) -> bool:
|
||||||
|
if _network_home.has_pending_local_decor_mutation():
|
||||||
|
_toolbar.report_status("waiting for the previous decor update")
|
||||||
|
return false
|
||||||
|
var owner_fingerprint: String = (
|
||||||
|
_network_home.get_local_home_owner_fingerprint()
|
||||||
|
)
|
||||||
|
var camera: Camera3D = _player.get_active_gameplay_camera()
|
||||||
|
var clicked_instance: String = _home_world.find_decor_instance_at_screen(
|
||||||
|
owner_fingerprint, camera, screen_position
|
||||||
|
)
|
||||||
|
if clicked_instance.is_empty():
|
||||||
|
return false
|
||||||
|
var placement: Dictionary = _home_state.get_placement(clicked_instance)
|
||||||
|
if placement.is_empty():
|
||||||
|
return false
|
||||||
|
var original_position: Vector3 = _placement_position(placement)
|
||||||
|
if not original_position.is_finite():
|
||||||
|
return false
|
||||||
|
if not _home_world.set_decor_manipulation_active(
|
||||||
|
owner_fingerprint, clicked_instance, true
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var pointer_position: Vector3 = _home_world.get_floor_position_from_screen(
|
||||||
|
owner_fingerprint, camera, screen_position
|
||||||
|
)
|
||||||
|
_drag_instance_id = clicked_instance
|
||||||
|
_drag_owner_fingerprint = owner_fingerprint
|
||||||
|
_drag_original_position = original_position
|
||||||
|
_drag_original_yaw = float(placement.get("yaw", 0.0))
|
||||||
|
_drag_target_position = original_position
|
||||||
|
_drag_target_yaw = _drag_original_yaw
|
||||||
|
_drag_target_valid = true
|
||||||
|
_drag_pointer_offset = (
|
||||||
|
original_position - pointer_position
|
||||||
|
if pointer_position.is_finite()
|
||||||
|
else Vector3.ZERO
|
||||||
|
)
|
||||||
|
_drag_pointer_offset.y = 0.0
|
||||||
|
_select_instance(clicked_instance)
|
||||||
|
_toolbar.report_status("drag decor, then release to place it")
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _update_drag(screen_position: Vector2) -> void:
|
||||||
|
if not _can_decorate():
|
||||||
|
_cancel_drag()
|
||||||
|
return
|
||||||
|
var camera: Camera3D = _player.get_active_gameplay_camera()
|
||||||
|
var pointer_position: Vector3 = _home_world.get_floor_position_from_screen(
|
||||||
|
_drag_owner_fingerprint, camera, screen_position
|
||||||
|
)
|
||||||
|
if not pointer_position.is_finite():
|
||||||
|
# Keep the most recent valid target when the pointer briefly leaves the
|
||||||
|
# RV floor. Releasing there should place at that target, not throw the
|
||||||
|
# decor all the way back to where the drag began.
|
||||||
|
_toolbar.report_status("keep the pointer over the RV floor")
|
||||||
|
return
|
||||||
|
var placement: Dictionary = _home_state.get_placement(_drag_instance_id)
|
||||||
|
if placement.is_empty():
|
||||||
|
_clear_selection()
|
||||||
|
return
|
||||||
|
var candidate: Vector3 = pointer_position + _drag_pointer_offset
|
||||||
|
candidate.y = 0.0
|
||||||
|
var normalized: Dictionary = _network_home.get_valid_local_decor_transform(
|
||||||
|
_drag_instance_id,
|
||||||
|
candidate,
|
||||||
|
float(placement.get("yaw", _drag_original_yaw)),
|
||||||
|
_mode,
|
||||||
|
)
|
||||||
|
if normalized.is_empty():
|
||||||
|
# A densely furnished area may have no nearby solution. Preserve the
|
||||||
|
# last valid preview so the interaction remains stable while the player
|
||||||
|
# moves the pointer toward open space.
|
||||||
|
_toolbar.report_status("holding at the nearest available position")
|
||||||
|
return
|
||||||
|
_drag_target_position = normalized["position"]
|
||||||
|
_drag_target_yaw = float(normalized["yaw"])
|
||||||
|
_drag_target_valid = true
|
||||||
|
_toolbar.report_status("release to place decor")
|
||||||
|
|
||||||
|
|
||||||
|
func _finish_drag() -> void:
|
||||||
|
var instance_id: String = _drag_instance_id
|
||||||
|
var owner_fingerprint: String = _drag_owner_fingerprint
|
||||||
|
var original_position: Vector3 = _drag_original_position
|
||||||
|
var original_yaw: float = _drag_original_yaw
|
||||||
|
var target_position: Vector3 = _drag_target_position
|
||||||
|
var target_yaw: float = _drag_target_yaw
|
||||||
|
var target_valid: bool = _drag_target_valid
|
||||||
|
var placement: Dictionary = _home_state.get_placement(instance_id)
|
||||||
|
var decor_id := StringName(str(placement.get("decor_id", "")))
|
||||||
|
if (
|
||||||
|
not target_valid
|
||||||
|
or decor_id.is_empty()
|
||||||
|
or not _home_world.is_decor_placement_clear_of_players(
|
||||||
|
owner_fingerprint, decor_id, target_position, target_yaw
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_home_world.set_decor_preview_transform(
|
||||||
|
owner_fingerprint,
|
||||||
|
instance_id,
|
||||||
|
original_position,
|
||||||
|
original_yaw,
|
||||||
|
)
|
||||||
|
_home_world.set_decor_manipulation_active(
|
||||||
|
owner_fingerprint, instance_id, false
|
||||||
|
)
|
||||||
|
_reset_drag_state()
|
||||||
|
_toolbar.report_status(
|
||||||
|
"a player is in the way"
|
||||||
|
if target_valid
|
||||||
|
else "that space is not available"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
_home_world.set_decor_preview_transform(
|
||||||
|
owner_fingerprint, instance_id, target_position, target_yaw
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
original_position.distance_to(target_position)
|
||||||
|
<= DRAG_POSITION_EPSILON
|
||||||
|
and is_zero_approx(angle_difference(original_yaw, target_yaw))
|
||||||
|
):
|
||||||
|
_home_world.set_decor_manipulation_active(
|
||||||
|
owner_fingerprint, instance_id, false
|
||||||
|
)
|
||||||
|
_reset_drag_state()
|
||||||
|
_select_instance(instance_id)
|
||||||
|
return
|
||||||
|
if _network_home.update_local_decor(
|
||||||
|
instance_id, target_position, target_yaw, _mode
|
||||||
|
):
|
||||||
|
_home_world.set_decor_manipulation_active(
|
||||||
|
owner_fingerprint, instance_id, false
|
||||||
|
)
|
||||||
|
_reset_drag_state()
|
||||||
|
_toolbar.report_status("decor moved")
|
||||||
|
return
|
||||||
|
_home_world.set_decor_preview_transform(
|
||||||
|
owner_fingerprint,
|
||||||
|
instance_id,
|
||||||
|
original_position,
|
||||||
|
original_yaw,
|
||||||
|
)
|
||||||
|
_home_world.set_decor_manipulation_active(
|
||||||
|
owner_fingerprint, instance_id, false
|
||||||
|
)
|
||||||
|
_reset_drag_state()
|
||||||
|
_toolbar.report_status("that space is not available")
|
||||||
|
|
||||||
|
|
||||||
|
func _cancel_drag() -> void:
|
||||||
|
if _drag_instance_id.is_empty():
|
||||||
|
return
|
||||||
|
_home_world.set_decor_preview_transform(
|
||||||
|
_drag_owner_fingerprint,
|
||||||
|
_drag_instance_id,
|
||||||
|
_drag_original_position,
|
||||||
|
_drag_original_yaw,
|
||||||
|
)
|
||||||
|
_home_world.set_decor_manipulation_active(
|
||||||
|
_drag_owner_fingerprint, _drag_instance_id, false
|
||||||
|
)
|
||||||
|
_reset_drag_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_drag_state() -> void:
|
||||||
|
_drag_instance_id = ""
|
||||||
|
_drag_owner_fingerprint = ""
|
||||||
|
_drag_pointer_offset = Vector3.ZERO
|
||||||
|
_drag_original_position = Vector3.ZERO
|
||||||
|
_drag_original_yaw = 0.0
|
||||||
|
_drag_target_position = Vector3.ZERO
|
||||||
|
_drag_target_yaw = 0.0
|
||||||
|
_drag_target_valid = false
|
||||||
|
|
||||||
|
|
||||||
|
static func _placement_position(placement: Dictionary) -> Vector3:
|
||||||
|
if placement.is_empty():
|
||||||
|
return Vector3.INF
|
||||||
|
var values: Array = placement.get("position", [])
|
||||||
|
if values.size() != 3:
|
||||||
|
return Vector3.INF
|
||||||
|
return Vector3(
|
||||||
|
float(values[0]), float(values[1]), float(values[2])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _place_carried_decor(decor_id: StringName) -> void:
|
||||||
|
if _network_home.has_pending_local_decor_mutation():
|
||||||
|
_toolbar.report_status("waiting for the previous decor update")
|
||||||
|
return
|
||||||
|
var owner_fingerprint: String = (
|
||||||
|
_network_home.get_local_home_owner_fingerprint()
|
||||||
|
)
|
||||||
|
# RV transitions keep the gameplay root axis-aligned so its transform can be
|
||||||
|
# synchronized independently from the presentation. Use the visual facing
|
||||||
|
# direction instead; otherwise carried decor is always placed along world -Z
|
||||||
|
# instead of in front of the character the player sees.
|
||||||
|
var forward: Vector3 = _player.get_facing_direction()
|
||||||
|
var global_position: Vector3 = _player.global_position + forward * 1.45
|
||||||
|
var local_position: Vector3 = _home_world.interior_global_to_local(
|
||||||
|
owner_fingerprint, global_position
|
||||||
|
)
|
||||||
|
var placement_yaw: float = atan2(-forward.x, -forward.z)
|
||||||
|
var instance_id: String = _network_home.place_local_decor(
|
||||||
|
decor_id, local_position, placement_yaw, _mode
|
||||||
|
)
|
||||||
|
if instance_id.is_empty():
|
||||||
|
_toolbar.report_status("make room in front of you to place this decor")
|
||||||
|
return
|
||||||
|
# A local host can reject its own optimistic manifest synchronously. Do not
|
||||||
|
# claim success after that rollback or leave the player looking for decor that
|
||||||
|
# was never accepted into the room.
|
||||||
|
if _home_state.get_placement(instance_id).is_empty():
|
||||||
|
_toolbar.report_status("decor placement was not accepted")
|
||||||
|
return
|
||||||
|
_select_instance(instance_id)
|
||||||
|
_toolbar.report_status("decor placed")
|
||||||
|
|
||||||
|
|
||||||
|
func _select_instance(instance_id: String) -> void:
|
||||||
|
var placement: Dictionary = _home_state.get_placement(instance_id)
|
||||||
|
if placement.is_empty():
|
||||||
|
_clear_selection()
|
||||||
|
return
|
||||||
|
_selected_instance_id = instance_id
|
||||||
|
var decor_id := StringName(str(placement.get("decor_id", "")))
|
||||||
|
_toolbar.set_selection(
|
||||||
|
instance_id, DecorCatalogType.get_display_name(decor_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _rotate_selected() -> void:
|
||||||
|
if not _can_decorate() or _selected_instance_id.is_empty():
|
||||||
|
return
|
||||||
|
if _network_home.has_pending_local_decor_mutation():
|
||||||
|
_toolbar.report_status("waiting for the previous decor update")
|
||||||
|
return
|
||||||
|
var placement: Dictionary = _home_state.get_placement(
|
||||||
|
_selected_instance_id
|
||||||
|
)
|
||||||
|
if placement.is_empty():
|
||||||
|
_clear_selection()
|
||||||
|
return
|
||||||
|
var values: Array = placement.get("position", [])
|
||||||
|
if values.size() != 3:
|
||||||
|
return
|
||||||
|
var position := Vector3(
|
||||||
|
float(values[0]), float(values[1]), float(values[2])
|
||||||
|
)
|
||||||
|
if _network_home.update_local_decor(
|
||||||
|
_selected_instance_id,
|
||||||
|
position,
|
||||||
|
float(placement.get("yaw", 0.0)) + PI * 0.5,
|
||||||
|
_mode,
|
||||||
|
):
|
||||||
|
_toolbar.report_status("decor rotated")
|
||||||
|
else:
|
||||||
|
_toolbar.report_status("not enough room to rotate here")
|
||||||
|
|
||||||
|
|
||||||
|
func _store_selected() -> void:
|
||||||
|
if _selected_instance_id.is_empty():
|
||||||
|
return
|
||||||
|
if _network_home.has_pending_local_decor_mutation():
|
||||||
|
_toolbar.report_status("waiting for the previous decor update")
|
||||||
|
return
|
||||||
|
if _network_home.remove_local_decor(_selected_instance_id):
|
||||||
|
_clear_selection()
|
||||||
|
_toolbar.report_status("decor packed into storage")
|
||||||
|
else:
|
||||||
|
_toolbar.report_status("inventory and storage are full")
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_selection() -> void:
|
||||||
|
_cancel_drag()
|
||||||
|
_selected_instance_id = ""
|
||||||
|
if _toolbar != null:
|
||||||
|
_toolbar.set_selection("")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_placement_mode_requested(mode: int) -> void:
|
||||||
|
_mode = mode as PlayerHomeState.PlacementMode
|
||||||
|
|
||||||
|
|
||||||
|
func _on_local_space_changed(
|
||||||
|
_space_id: StringName,
|
||||||
|
_owner_fingerprint: String,
|
||||||
|
) -> void:
|
||||||
|
_clear_selection()
|
||||||
|
_refresh_toolbar()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_local_manifest_rejected(message: String) -> void:
|
||||||
|
_clear_selection()
|
||||||
|
if _toolbar != null and _toolbar.visible:
|
||||||
|
_toolbar.report_status(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_home_changed() -> void:
|
||||||
|
if (
|
||||||
|
not _selected_instance_id.is_empty()
|
||||||
|
and _home_state.get_placement(_selected_instance_id).is_empty()
|
||||||
|
):
|
||||||
|
_clear_selection()
|
||||||
|
_refresh_toolbar()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_toolbar() -> void:
|
||||||
|
if _toolbar == null:
|
||||||
|
return
|
||||||
|
_toolbar.set_active(_can_decorate())
|
||||||
|
if not _toolbar.visible:
|
||||||
|
_clear_selection()
|
||||||
|
|
||||||
|
|
||||||
|
func _can_decorate() -> bool:
|
||||||
|
return (
|
||||||
|
_player != null
|
||||||
|
and _network_home != null
|
||||||
|
and _network_home.can_local_decorate()
|
||||||
|
and _fishing_spot != null
|
||||||
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
|
and _player.is_movement_enabled()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_selected_decor_id() -> StringName:
|
||||||
|
if (
|
||||||
|
_hotbar == null
|
||||||
|
or _hotbar.get_selected_assignment_kind()
|
||||||
|
!= PlayerHotbar.AssignmentKind.ITEM
|
||||||
|
):
|
||||||
|
return StringName()
|
||||||
|
var item_id: StringName = _hotbar.get_selected_item_id()
|
||||||
|
return item_id if DecorCatalogType.has_product(item_id) else StringName()
|
||||||
1
homes/home_decor_controller.gd.uid
Normal file
1
homes/home_decor_controller.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b5j0mqcvhmi54
|
||||||
1209
homes/home_world_service.gd
Normal file
1209
homes/home_world_service.gd
Normal file
File diff suppressed because it is too large
Load diff
1
homes/home_world_service.gd.uid
Normal file
1
homes/home_world_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cy2kjoxhwxujd
|
||||||
786
homes/player_home_state.gd
Normal file
786
homes/player_home_state.gd
Normal file
|
|
@ -0,0 +1,786 @@
|
||||||
|
class_name PlayerHomeState
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal changed
|
||||||
|
|
||||||
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||||
|
|
||||||
|
const HOME_SCHEMA_VERSION: int = 1
|
||||||
|
const STARTER_TIER: int = 0
|
||||||
|
const STARTER_EXTERIOR_ID: StringName = &"starter_motorcoach"
|
||||||
|
const MAX_TIER: int = 8
|
||||||
|
const MAX_PLACEMENTS: int = 128
|
||||||
|
const MAX_INSTANCE_ID_LENGTH: int = 96
|
||||||
|
const MAX_REVISION: int = 2147483647
|
||||||
|
const GRID_SIZE: float = 0.5
|
||||||
|
const FREE_PLACEMENT_SEARCH_STEP: float = 0.1
|
||||||
|
const FREE_PLACEMENT_SEARCH_ANGLES: int = 24
|
||||||
|
const NEAREST_PLACEMENT_SEARCH_DISTANCE: float = 2.0
|
||||||
|
# The starter RV is intentionally cramped. Later RV tiers can replace these
|
||||||
|
# bounds with larger layouts when tiered interiors are introduced.
|
||||||
|
# The starter interior is 8.0 m by 4.5 m with 0.18 m walls. These bounds
|
||||||
|
# follow the walls' inner faces, with a small visual gap to avoid z-fighting.
|
||||||
|
const INTERIOR_HALF_EXTENTS := Vector2(3.91, 2.16)
|
||||||
|
const PLACEMENT_EDGE_PADDING: float = 0.02
|
||||||
|
|
||||||
|
enum Privacy {
|
||||||
|
OPEN,
|
||||||
|
CLOSED,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlacementMode {
|
||||||
|
GRID,
|
||||||
|
FREE,
|
||||||
|
}
|
||||||
|
|
||||||
|
var _tier: int = STARTER_TIER
|
||||||
|
var _exterior_id: StringName = STARTER_EXTERIOR_ID
|
||||||
|
var _privacy: Privacy = Privacy.OPEN
|
||||||
|
var _owned_decor: Dictionary[StringName, int] = {}
|
||||||
|
var _placements: Array[Dictionary] = []
|
||||||
|
var _revision: int = 0
|
||||||
|
var _next_instance_sequence: int = 1
|
||||||
|
|
||||||
|
|
||||||
|
func get_tier() -> int:
|
||||||
|
return _tier
|
||||||
|
|
||||||
|
|
||||||
|
func get_exterior_id() -> StringName:
|
||||||
|
return _exterior_id
|
||||||
|
|
||||||
|
|
||||||
|
func get_privacy() -> Privacy:
|
||||||
|
return _privacy
|
||||||
|
|
||||||
|
|
||||||
|
func get_revision() -> int:
|
||||||
|
return _revision
|
||||||
|
|
||||||
|
|
||||||
|
func get_owned_count(decor_id: StringName) -> int:
|
||||||
|
return int(_owned_decor.get(decor_id, 0))
|
||||||
|
|
||||||
|
|
||||||
|
func get_placed_count(decor_id: StringName) -> int:
|
||||||
|
var count: int = 0
|
||||||
|
for placement: Dictionary in _placements:
|
||||||
|
if StringName(str(placement.get("decor_id", ""))) == decor_id:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
func get_available_count(decor_id: StringName) -> int:
|
||||||
|
return maxi(
|
||||||
|
get_owned_count(decor_id) - get_placed_count(decor_id),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_owned_decor() -> Dictionary[StringName, int]:
|
||||||
|
return _owned_decor.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func get_placements() -> Array[Dictionary]:
|
||||||
|
var result: Array[Dictionary] = []
|
||||||
|
for placement: Dictionary in _placements:
|
||||||
|
result.append(placement.duplicate(true))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func get_placement(instance_id: String) -> Dictionary:
|
||||||
|
var index: int = _placement_index(instance_id)
|
||||||
|
return _placements[index].duplicate(true) if index >= 0 else {}
|
||||||
|
|
||||||
|
|
||||||
|
func can_place_decor(
|
||||||
|
decor_id: StringName,
|
||||||
|
position: Vector3,
|
||||||
|
yaw_radians: float,
|
||||||
|
mode: PlacementMode,
|
||||||
|
) -> bool:
|
||||||
|
if (
|
||||||
|
get_available_count(decor_id) <= 0
|
||||||
|
or _placements.size() >= MAX_PLACEMENTS
|
||||||
|
or not _valid_mode(mode)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return not get_nearest_valid_placement_transform(
|
||||||
|
decor_id,
|
||||||
|
position,
|
||||||
|
yaw_radians,
|
||||||
|
mode,
|
||||||
|
"",
|
||||||
|
).is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func set_privacy(value: Privacy) -> bool:
|
||||||
|
if value < Privacy.OPEN or value > Privacy.CLOSED:
|
||||||
|
return false
|
||||||
|
if _privacy == value:
|
||||||
|
return true
|
||||||
|
_privacy = value
|
||||||
|
_touch()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func add_owned_decor(
|
||||||
|
decor_id: StringName,
|
||||||
|
quantity: int = 1,
|
||||||
|
) -> bool:
|
||||||
|
if (
|
||||||
|
not DecorCatalogType.has_product(decor_id)
|
||||||
|
or quantity <= 0
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var current: int = get_owned_count(decor_id)
|
||||||
|
if current + quantity > DecorCatalogType.MAX_OWNED_PER_PRODUCT:
|
||||||
|
return false
|
||||||
|
_owned_decor[decor_id] = current + quantity
|
||||||
|
_touch()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func remove_owned_decor(
|
||||||
|
decor_id: StringName,
|
||||||
|
quantity: int = 1,
|
||||||
|
) -> bool:
|
||||||
|
if quantity <= 0:
|
||||||
|
return false
|
||||||
|
var current: int = get_owned_count(decor_id)
|
||||||
|
if current - quantity < get_placed_count(decor_id):
|
||||||
|
return false
|
||||||
|
var remaining: int = current - quantity
|
||||||
|
if remaining < 0:
|
||||||
|
return false
|
||||||
|
if remaining == 0:
|
||||||
|
_owned_decor.erase(decor_id)
|
||||||
|
else:
|
||||||
|
_owned_decor[decor_id] = remaining
|
||||||
|
_touch()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func place_decor(
|
||||||
|
decor_id: StringName,
|
||||||
|
position: Vector3,
|
||||||
|
yaw_radians: float,
|
||||||
|
mode: PlacementMode,
|
||||||
|
) -> String:
|
||||||
|
if (
|
||||||
|
get_available_count(decor_id) <= 0
|
||||||
|
or _placements.size() >= MAX_PLACEMENTS
|
||||||
|
or not _valid_mode(mode)
|
||||||
|
):
|
||||||
|
return ""
|
||||||
|
var normalized := get_nearest_valid_placement_transform(
|
||||||
|
decor_id,
|
||||||
|
position,
|
||||||
|
yaw_radians,
|
||||||
|
mode,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
if normalized.is_empty():
|
||||||
|
return ""
|
||||||
|
var instance_id := _make_instance_id()
|
||||||
|
var placement: Dictionary = {
|
||||||
|
"instance_id": instance_id,
|
||||||
|
"decor_id": String(decor_id),
|
||||||
|
"mode": int(mode),
|
||||||
|
"position": _vector3_to_array(normalized["position"]),
|
||||||
|
"yaw": float(normalized["yaw"]),
|
||||||
|
}
|
||||||
|
_placements.append(placement)
|
||||||
|
_touch()
|
||||||
|
return instance_id
|
||||||
|
|
||||||
|
|
||||||
|
func update_placement(
|
||||||
|
instance_id: String,
|
||||||
|
position: Vector3,
|
||||||
|
yaw_radians: float,
|
||||||
|
mode: PlacementMode,
|
||||||
|
) -> bool:
|
||||||
|
var index: int = _placement_index(instance_id)
|
||||||
|
var normalized: Dictionary = get_valid_update_transform(
|
||||||
|
instance_id, position, yaw_radians, mode
|
||||||
|
)
|
||||||
|
if index < 0 or normalized.is_empty():
|
||||||
|
return false
|
||||||
|
var placement: Dictionary = _placements[index].duplicate(true)
|
||||||
|
placement["mode"] = int(mode)
|
||||||
|
placement["position"] = _vector3_to_array(normalized["position"])
|
||||||
|
placement["yaw"] = float(normalized["yaw"])
|
||||||
|
_placements[index] = placement
|
||||||
|
_touch()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_valid_update_transform(
|
||||||
|
instance_id: String,
|
||||||
|
position: Vector3,
|
||||||
|
yaw_radians: float,
|
||||||
|
mode: PlacementMode,
|
||||||
|
candidate_validator: Callable = Callable(),
|
||||||
|
) -> Dictionary:
|
||||||
|
var index: int = _placement_index(instance_id)
|
||||||
|
if index < 0 or not _valid_mode(mode):
|
||||||
|
return {}
|
||||||
|
var decor_id := StringName(
|
||||||
|
str(_placements[index].get("decor_id", ""))
|
||||||
|
)
|
||||||
|
return get_nearest_valid_placement_transform(
|
||||||
|
decor_id,
|
||||||
|
position,
|
||||||
|
yaw_radians,
|
||||||
|
mode,
|
||||||
|
instance_id,
|
||||||
|
candidate_validator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_nearest_valid_placement_transform(
|
||||||
|
decor_id: StringName,
|
||||||
|
position: Vector3,
|
||||||
|
yaw_radians: float,
|
||||||
|
mode: PlacementMode,
|
||||||
|
ignored_instance_id: String = "",
|
||||||
|
candidate_validator: Callable = Callable(),
|
||||||
|
) -> Dictionary:
|
||||||
|
var normalized: Dictionary = normalized_placement_transform(
|
||||||
|
decor_id, position, yaw_radians, mode
|
||||||
|
)
|
||||||
|
if normalized.is_empty():
|
||||||
|
return {}
|
||||||
|
if (
|
||||||
|
not _overlaps_existing(
|
||||||
|
decor_id, normalized, ignored_instance_id
|
||||||
|
)
|
||||||
|
and _candidate_is_accepted(normalized, candidate_validator)
|
||||||
|
):
|
||||||
|
return normalized
|
||||||
|
return (
|
||||||
|
_find_nearest_grid_transform(
|
||||||
|
decor_id,
|
||||||
|
normalized,
|
||||||
|
ignored_instance_id,
|
||||||
|
candidate_validator,
|
||||||
|
)
|
||||||
|
if mode == PlacementMode.GRID
|
||||||
|
else _find_nearest_free_transform(
|
||||||
|
decor_id,
|
||||||
|
normalized,
|
||||||
|
ignored_instance_id,
|
||||||
|
candidate_validator,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func remove_placement(instance_id: String) -> bool:
|
||||||
|
var index: int = _placement_index(instance_id)
|
||||||
|
if index < 0:
|
||||||
|
return false
|
||||||
|
_placements.remove_at(index)
|
||||||
|
_touch()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func reset_to_defaults() -> void:
|
||||||
|
_tier = STARTER_TIER
|
||||||
|
_exterior_id = STARTER_EXTERIOR_ID
|
||||||
|
_privacy = Privacy.OPEN
|
||||||
|
_owned_decor.clear()
|
||||||
|
_placements.clear()
|
||||||
|
_revision = 0
|
||||||
|
_next_instance_sequence = 1
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func to_save_data() -> Dictionary:
|
||||||
|
var owned: Dictionary = {}
|
||||||
|
var ids: Array[StringName] = []
|
||||||
|
ids.assign(_owned_decor.keys())
|
||||||
|
ids.sort_custom(func(first: StringName, second: StringName) -> bool:
|
||||||
|
return String(first) < String(second)
|
||||||
|
)
|
||||||
|
for decor_id: StringName in ids:
|
||||||
|
owned[String(decor_id)] = _owned_decor[decor_id]
|
||||||
|
return {
|
||||||
|
"schema_version": HOME_SCHEMA_VERSION,
|
||||||
|
"tier": _tier,
|
||||||
|
"exterior_id": String(_exterior_id),
|
||||||
|
"privacy": int(_privacy),
|
||||||
|
"owned_decor": owned,
|
||||||
|
"placements": get_placements(),
|
||||||
|
"revision": _revision,
|
||||||
|
"next_instance_sequence": _next_instance_sequence,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func restore_from_save_data(data: Dictionary) -> bool:
|
||||||
|
var sanitized: Dictionary = sanitize_save_data(data)
|
||||||
|
if sanitized.is_empty():
|
||||||
|
return false
|
||||||
|
_tier = int(sanitized["tier"])
|
||||||
|
_exterior_id = StringName(str(sanitized["exterior_id"]))
|
||||||
|
_privacy = int(sanitized["privacy"])
|
||||||
|
_owned_decor.clear()
|
||||||
|
for key: Variant in (sanitized["owned_decor"] as Dictionary):
|
||||||
|
_owned_decor[StringName(str(key))] = int(
|
||||||
|
(sanitized["owned_decor"] as Dictionary)[key]
|
||||||
|
)
|
||||||
|
_placements.clear()
|
||||||
|
for value: Variant in sanitized["placements"] as Array:
|
||||||
|
_placements.append((value as Dictionary).duplicate(true))
|
||||||
|
_revision = int(sanitized["revision"])
|
||||||
|
_next_instance_sequence = int(sanitized["next_instance_sequence"])
|
||||||
|
changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
static func default_save_data() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"schema_version": HOME_SCHEMA_VERSION,
|
||||||
|
"tier": STARTER_TIER,
|
||||||
|
"exterior_id": String(STARTER_EXTERIOR_ID),
|
||||||
|
"privacy": int(Privacy.OPEN),
|
||||||
|
"owned_decor": {},
|
||||||
|
"placements": [],
|
||||||
|
"revision": 0,
|
||||||
|
"next_instance_sequence": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func sanitize_save_data(data: Dictionary) -> Dictionary:
|
||||||
|
if (
|
||||||
|
not _is_integer_value(data.get("schema_version"))
|
||||||
|
or int(data.get("schema_version", -1)) != HOME_SCHEMA_VERSION
|
||||||
|
or not _is_integer_value(data.get("tier"))
|
||||||
|
or typeof(data.get("exterior_id")) not in [
|
||||||
|
TYPE_STRING, TYPE_STRING_NAME,
|
||||||
|
]
|
||||||
|
or not _is_integer_value(data.get("privacy"))
|
||||||
|
or typeof(data.get("owned_decor")) != TYPE_DICTIONARY
|
||||||
|
or typeof(data.get("placements")) != TYPE_ARRAY
|
||||||
|
or not _is_integer_value(data.get("revision"))
|
||||||
|
or not _is_integer_value(data.get("next_instance_sequence"))
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var tier: int = int(data["tier"])
|
||||||
|
var exterior_id: String = str(data["exterior_id"])
|
||||||
|
var privacy: int = int(data["privacy"])
|
||||||
|
var revision: int = int(data["revision"])
|
||||||
|
var next_sequence: int = int(data["next_instance_sequence"])
|
||||||
|
if (
|
||||||
|
tier < STARTER_TIER
|
||||||
|
or tier > MAX_TIER
|
||||||
|
or exterior_id.is_empty()
|
||||||
|
or exterior_id.length() > 96
|
||||||
|
or privacy < Privacy.OPEN
|
||||||
|
or privacy > Privacy.CLOSED
|
||||||
|
or revision < 0
|
||||||
|
or revision > MAX_REVISION
|
||||||
|
or next_sequence < 1
|
||||||
|
or next_sequence > MAX_REVISION
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var sanitized_owned: Dictionary = {}
|
||||||
|
var owned: Dictionary = data["owned_decor"]
|
||||||
|
for key: Variant in owned:
|
||||||
|
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
return {}
|
||||||
|
var decor_id := StringName(str(key))
|
||||||
|
var quantity_value: Variant = owned[key]
|
||||||
|
if (
|
||||||
|
not DecorCatalogType.has_product(decor_id)
|
||||||
|
or not _is_integer_value(quantity_value)
|
||||||
|
or int(quantity_value) <= 0
|
||||||
|
or int(quantity_value) > DecorCatalogType.MAX_OWNED_PER_PRODUCT
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
sanitized_owned[String(decor_id)] = int(quantity_value)
|
||||||
|
var values: Array = data["placements"]
|
||||||
|
if values.size() > MAX_PLACEMENTS:
|
||||||
|
return {}
|
||||||
|
var sanitized_placements: Array[Dictionary] = []
|
||||||
|
var seen_instance_ids: Dictionary[String, bool] = {}
|
||||||
|
var placed_counts: Dictionary[StringName, int] = {}
|
||||||
|
for value: Variant in values:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return {}
|
||||||
|
var sanitized: Dictionary = sanitize_placement(value)
|
||||||
|
if sanitized.is_empty():
|
||||||
|
return {}
|
||||||
|
var instance_id: String = str(sanitized["instance_id"])
|
||||||
|
var decor_id := StringName(str(sanitized["decor_id"]))
|
||||||
|
if seen_instance_ids.has(instance_id):
|
||||||
|
return {}
|
||||||
|
seen_instance_ids[instance_id] = true
|
||||||
|
placed_counts[decor_id] = int(
|
||||||
|
placed_counts.get(decor_id, 0)
|
||||||
|
) + 1
|
||||||
|
if (
|
||||||
|
int(placed_counts[decor_id])
|
||||||
|
> int(sanitized_owned.get(String(decor_id), 0))
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
for previous: Dictionary in sanitized_placements:
|
||||||
|
if placements_overlap(previous, sanitized):
|
||||||
|
return {}
|
||||||
|
sanitized_placements.append(sanitized)
|
||||||
|
return {
|
||||||
|
"schema_version": HOME_SCHEMA_VERSION,
|
||||||
|
"tier": tier,
|
||||||
|
"exterior_id": exterior_id,
|
||||||
|
"privacy": privacy,
|
||||||
|
"owned_decor": sanitized_owned,
|
||||||
|
"placements": sanitized_placements,
|
||||||
|
"revision": revision,
|
||||||
|
"next_instance_sequence": next_sequence,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func sanitize_placement(value: Dictionary) -> Dictionary:
|
||||||
|
for key: String in [
|
||||||
|
"instance_id", "decor_id", "mode", "position", "yaw",
|
||||||
|
]:
|
||||||
|
if not value.has(key):
|
||||||
|
return {}
|
||||||
|
if (
|
||||||
|
typeof(value["instance_id"]) != TYPE_STRING
|
||||||
|
or typeof(value["decor_id"]) not in [
|
||||||
|
TYPE_STRING, TYPE_STRING_NAME,
|
||||||
|
]
|
||||||
|
or not _is_integer_value(value["mode"])
|
||||||
|
or typeof(value["position"]) != TYPE_ARRAY
|
||||||
|
or typeof(value["yaw"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var instance_id: String = value["instance_id"]
|
||||||
|
var decor_id := StringName(str(value["decor_id"]))
|
||||||
|
var mode: int = int(value["mode"])
|
||||||
|
var position_values: Array = value["position"]
|
||||||
|
var yaw: float = float(value["yaw"])
|
||||||
|
if (
|
||||||
|
instance_id.is_empty()
|
||||||
|
or instance_id.length() > MAX_INSTANCE_ID_LENGTH
|
||||||
|
or not DecorCatalogType.has_product(decor_id)
|
||||||
|
or not _valid_mode(mode)
|
||||||
|
or position_values.size() != 3
|
||||||
|
or not is_finite(yaw)
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
for component: Variant in position_values:
|
||||||
|
if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
|
||||||
|
return {}
|
||||||
|
var position := Vector3(
|
||||||
|
float(position_values[0]),
|
||||||
|
float(position_values[1]),
|
||||||
|
float(position_values[2]),
|
||||||
|
)
|
||||||
|
var normalized := normalized_placement_transform(
|
||||||
|
decor_id,
|
||||||
|
position,
|
||||||
|
yaw,
|
||||||
|
mode as PlacementMode,
|
||||||
|
)
|
||||||
|
if normalized.is_empty():
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"instance_id": instance_id,
|
||||||
|
"decor_id": String(decor_id),
|
||||||
|
"mode": mode,
|
||||||
|
"position": _vector3_to_array(normalized["position"]),
|
||||||
|
"yaw": float(normalized["yaw"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func _is_integer_value(value: Variant) -> bool:
|
||||||
|
if typeof(value) == TYPE_INT:
|
||||||
|
return true
|
||||||
|
if typeof(value) != TYPE_FLOAT:
|
||||||
|
return false
|
||||||
|
var number: float = float(value)
|
||||||
|
return is_finite(number) and is_equal_approx(number, roundf(number))
|
||||||
|
|
||||||
|
|
||||||
|
static func normalized_placement_transform(
|
||||||
|
decor_id: StringName,
|
||||||
|
position: Vector3,
|
||||||
|
yaw_radians: float,
|
||||||
|
mode: PlacementMode,
|
||||||
|
) -> Dictionary:
|
||||||
|
if (
|
||||||
|
not DecorCatalogType.has_product(decor_id)
|
||||||
|
or not position.is_finite()
|
||||||
|
or not is_finite(yaw_radians)
|
||||||
|
or not _valid_mode(mode)
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var normalized_position := Vector3(position.x, 0.0, position.z)
|
||||||
|
var normalized_yaw: float = wrapf(yaw_radians, -PI, PI)
|
||||||
|
if mode == PlacementMode.GRID:
|
||||||
|
normalized_position.x = snappedf(normalized_position.x, GRID_SIZE)
|
||||||
|
normalized_position.z = snappedf(normalized_position.z, GRID_SIZE)
|
||||||
|
normalized_yaw = snappedf(normalized_yaw, PI * 0.5)
|
||||||
|
var footprint: Vector2 = DecorCatalogType.get_footprint(decor_id)
|
||||||
|
var cosine: float = absf(cos(normalized_yaw))
|
||||||
|
var sine: float = absf(sin(normalized_yaw))
|
||||||
|
var half_size := Vector2(
|
||||||
|
(footprint.x * cosine + footprint.y * sine) * 0.5,
|
||||||
|
(footprint.x * sine + footprint.y * cosine) * 0.5,
|
||||||
|
)
|
||||||
|
var maximum_x: float = (
|
||||||
|
INTERIOR_HALF_EXTENTS.x - half_size.x - PLACEMENT_EDGE_PADDING
|
||||||
|
)
|
||||||
|
var maximum_z: float = (
|
||||||
|
INTERIOR_HALF_EXTENTS.y - half_size.y - PLACEMENT_EDGE_PADDING
|
||||||
|
)
|
||||||
|
if maximum_x < 0.0 or maximum_z < 0.0:
|
||||||
|
return {}
|
||||||
|
# Clamp after grid snapping so a grid-placed item can still sit flush at a
|
||||||
|
# wall even when the wall itself is not on an exact grid interval.
|
||||||
|
normalized_position.x = clampf(
|
||||||
|
normalized_position.x, -maximum_x, maximum_x
|
||||||
|
)
|
||||||
|
normalized_position.z = clampf(
|
||||||
|
normalized_position.z, -maximum_z, maximum_z
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"position": normalized_position,
|
||||||
|
"yaw": normalized_yaw,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _find_nearest_grid_transform(
|
||||||
|
decor_id: StringName,
|
||||||
|
origin: Dictionary,
|
||||||
|
ignored_instance_id: String,
|
||||||
|
candidate_validator: Callable,
|
||||||
|
) -> Dictionary:
|
||||||
|
var origin_position: Vector3 = origin["position"]
|
||||||
|
var yaw: float = float(origin["yaw"])
|
||||||
|
var maximum_ring: int = ceili(
|
||||||
|
NEAREST_PLACEMENT_SEARCH_DISTANCE / GRID_SIZE
|
||||||
|
)
|
||||||
|
var seen: Dictionary[String, bool] = {}
|
||||||
|
for ring: int in range(1, maximum_ring + 1):
|
||||||
|
var nearest: Dictionary = {}
|
||||||
|
var nearest_distance_squared: float = INF
|
||||||
|
for x_offset: int in range(-ring, ring + 1):
|
||||||
|
for z_offset: int in range(-ring, ring + 1):
|
||||||
|
if maxi(absi(x_offset), absi(z_offset)) != ring:
|
||||||
|
continue
|
||||||
|
var candidate: Dictionary = normalized_placement_transform(
|
||||||
|
decor_id,
|
||||||
|
origin_position + Vector3(
|
||||||
|
float(x_offset) * GRID_SIZE,
|
||||||
|
0.0,
|
||||||
|
float(z_offset) * GRID_SIZE,
|
||||||
|
),
|
||||||
|
yaw,
|
||||||
|
PlacementMode.GRID,
|
||||||
|
)
|
||||||
|
if candidate.is_empty() or not _mark_candidate_unseen(
|
||||||
|
candidate, seen
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if _overlaps_existing(
|
||||||
|
decor_id, candidate, ignored_instance_id
|
||||||
|
) or not _candidate_is_accepted(
|
||||||
|
candidate, candidate_validator
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var candidate_position: Vector3 = candidate["position"]
|
||||||
|
var distance_squared: float = origin_position.distance_squared_to(
|
||||||
|
candidate_position
|
||||||
|
)
|
||||||
|
if distance_squared < nearest_distance_squared:
|
||||||
|
nearest = candidate
|
||||||
|
nearest_distance_squared = distance_squared
|
||||||
|
if not nearest.is_empty():
|
||||||
|
return nearest
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _find_nearest_free_transform(
|
||||||
|
decor_id: StringName,
|
||||||
|
origin: Dictionary,
|
||||||
|
ignored_instance_id: String,
|
||||||
|
candidate_validator: Callable,
|
||||||
|
) -> Dictionary:
|
||||||
|
var origin_position: Vector3 = origin["position"]
|
||||||
|
var yaw: float = float(origin["yaw"])
|
||||||
|
var maximum_ring: int = ceili(
|
||||||
|
NEAREST_PLACEMENT_SEARCH_DISTANCE / FREE_PLACEMENT_SEARCH_STEP
|
||||||
|
)
|
||||||
|
var seen: Dictionary[String, bool] = {}
|
||||||
|
for ring: int in range(1, maximum_ring + 1):
|
||||||
|
var radius: float = float(ring) * FREE_PLACEMENT_SEARCH_STEP
|
||||||
|
var nearest: Dictionary = {}
|
||||||
|
var nearest_distance_squared: float = INF
|
||||||
|
for angle_index: int in FREE_PLACEMENT_SEARCH_ANGLES:
|
||||||
|
var angle: float = (
|
||||||
|
TAU * float(angle_index) / float(FREE_PLACEMENT_SEARCH_ANGLES)
|
||||||
|
)
|
||||||
|
var candidate: Dictionary = normalized_placement_transform(
|
||||||
|
decor_id,
|
||||||
|
origin_position + Vector3(
|
||||||
|
cos(angle) * radius, 0.0, sin(angle) * radius
|
||||||
|
),
|
||||||
|
yaw,
|
||||||
|
PlacementMode.FREE,
|
||||||
|
)
|
||||||
|
if candidate.is_empty() or not _mark_candidate_unseen(
|
||||||
|
candidate, seen
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if _overlaps_existing(
|
||||||
|
decor_id, candidate, ignored_instance_id
|
||||||
|
) or not _candidate_is_accepted(
|
||||||
|
candidate, candidate_validator
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var candidate_position: Vector3 = candidate["position"]
|
||||||
|
var distance_squared: float = origin_position.distance_squared_to(
|
||||||
|
candidate_position
|
||||||
|
)
|
||||||
|
if distance_squared < nearest_distance_squared:
|
||||||
|
nearest = candidate
|
||||||
|
nearest_distance_squared = distance_squared
|
||||||
|
if not nearest.is_empty():
|
||||||
|
return nearest
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
static func _mark_candidate_unseen(
|
||||||
|
candidate: Dictionary,
|
||||||
|
seen: Dictionary[String, bool],
|
||||||
|
) -> bool:
|
||||||
|
var position: Vector3 = candidate["position"]
|
||||||
|
var key := "%d:%d" % [
|
||||||
|
roundi(position.x * 10000.0),
|
||||||
|
roundi(position.z * 10000.0),
|
||||||
|
]
|
||||||
|
if seen.has(key):
|
||||||
|
return false
|
||||||
|
seen[key] = true
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
static func _candidate_is_accepted(
|
||||||
|
candidate: Dictionary,
|
||||||
|
validator: Callable,
|
||||||
|
) -> bool:
|
||||||
|
return not validator.is_valid() or bool(validator.call(candidate))
|
||||||
|
|
||||||
|
|
||||||
|
static func placements_overlap(first: Dictionary, second: Dictionary) -> bool:
|
||||||
|
var first_id := StringName(str(first.get("decor_id", "")))
|
||||||
|
var second_id := StringName(str(second.get("decor_id", "")))
|
||||||
|
if (
|
||||||
|
not DecorCatalogType.has_product(first_id)
|
||||||
|
or not DecorCatalogType.has_product(second_id)
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
var first_position: Vector3 = _array_to_vector3(first.get("position", []))
|
||||||
|
var second_position: Vector3 = _array_to_vector3(second.get("position", []))
|
||||||
|
var first_yaw: float = float(first.get("yaw", 0.0))
|
||||||
|
var second_yaw: float = float(second.get("yaw", 0.0))
|
||||||
|
var first_half: Vector2 = (
|
||||||
|
DecorCatalogType.get_footprint(first_id) * 0.5
|
||||||
|
)
|
||||||
|
var second_half: Vector2 = (
|
||||||
|
DecorCatalogType.get_footprint(second_id) * 0.5
|
||||||
|
)
|
||||||
|
var first_axes: Array[Vector2] = _placement_axes(first_yaw)
|
||||||
|
var second_axes: Array[Vector2] = _placement_axes(second_yaw)
|
||||||
|
var delta := Vector2(
|
||||||
|
second_position.x - first_position.x,
|
||||||
|
second_position.z - first_position.z,
|
||||||
|
)
|
||||||
|
for axis: Vector2 in [
|
||||||
|
first_axes[0], first_axes[1], second_axes[0], second_axes[1],
|
||||||
|
]:
|
||||||
|
var first_radius: float = (
|
||||||
|
absf(axis.dot(first_axes[0])) * first_half.x
|
||||||
|
+ absf(axis.dot(first_axes[1])) * first_half.y
|
||||||
|
)
|
||||||
|
var second_radius: float = (
|
||||||
|
absf(axis.dot(second_axes[0])) * second_half.x
|
||||||
|
+ absf(axis.dot(second_axes[1])) * second_half.y
|
||||||
|
)
|
||||||
|
if absf(delta.dot(axis)) >= (
|
||||||
|
first_radius + second_radius - 0.001
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _make_instance_id() -> String:
|
||||||
|
var result := "decor-%d-%d" % [
|
||||||
|
Time.get_ticks_usec(),
|
||||||
|
_next_instance_sequence,
|
||||||
|
]
|
||||||
|
_next_instance_sequence = mini(
|
||||||
|
_next_instance_sequence + 1,
|
||||||
|
MAX_REVISION,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _placement_index(instance_id: String) -> int:
|
||||||
|
if instance_id.is_empty():
|
||||||
|
return -1
|
||||||
|
for index: int in _placements.size():
|
||||||
|
if str(_placements[index].get("instance_id", "")) == instance_id:
|
||||||
|
return index
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
func _overlaps_existing(
|
||||||
|
decor_id: StringName,
|
||||||
|
normalized: Dictionary,
|
||||||
|
ignored_instance_id: String,
|
||||||
|
) -> bool:
|
||||||
|
var candidate: Dictionary = {
|
||||||
|
"decor_id": String(decor_id),
|
||||||
|
"position": _vector3_to_array(normalized["position"]),
|
||||||
|
"yaw": float(normalized["yaw"]),
|
||||||
|
}
|
||||||
|
for placement: Dictionary in _placements:
|
||||||
|
if str(placement.get("instance_id", "")) == ignored_instance_id:
|
||||||
|
continue
|
||||||
|
if placements_overlap(candidate, placement):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _touch() -> void:
|
||||||
|
_revision = mini(_revision + 1, MAX_REVISION)
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
static func _valid_mode(value: int) -> bool:
|
||||||
|
return value >= PlacementMode.GRID and value <= PlacementMode.FREE
|
||||||
|
|
||||||
|
|
||||||
|
static func _vector3_to_array(value: Vector3) -> Array[float]:
|
||||||
|
return [value.x, value.y, value.z]
|
||||||
|
|
||||||
|
|
||||||
|
static func _array_to_vector3(value: Variant) -> Vector3:
|
||||||
|
if typeof(value) != TYPE_ARRAY or (value as Array).size() != 3:
|
||||||
|
return Vector3.INF
|
||||||
|
var components: Array = value
|
||||||
|
return Vector3(
|
||||||
|
float(components[0]),
|
||||||
|
float(components[1]),
|
||||||
|
float(components[2]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _placement_axes(yaw: float) -> Array[Vector2]:
|
||||||
|
var cosine: float = cos(yaw)
|
||||||
|
var sine: float = sin(yaw)
|
||||||
|
return [Vector2(cosine, sine), Vector2(-sine, cosine)]
|
||||||
1
homes/player_home_state.gd.uid
Normal file
1
homes/player_home_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ggfdoak2ixxx
|
||||||
|
|
@ -60,6 +60,28 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func add_item_to_storage(item_id: StringName, quantity: int = 1) -> bool:
|
||||||
|
var existing: OwnedItemType = get_owned_item(item_id)
|
||||||
|
if existing != null:
|
||||||
|
return add_item(item_id, quantity)
|
||||||
|
var item: ItemDataType = _resolve_available_item(item_id)
|
||||||
|
if (
|
||||||
|
item == null
|
||||||
|
or quantity <= 0
|
||||||
|
or quantity > (item.max_stack if item.stackable else 1)
|
||||||
|
or _inventory_layout == null
|
||||||
|
or not _inventory_layout.can_accept_item_in_storage(item_id)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var owned := OwnedItemType.new()
|
||||||
|
owned.item_id = item_id
|
||||||
|
owned.quantity = quantity
|
||||||
|
owned.storage_slot = _first_available_storage_slot(item, _items)
|
||||||
|
_items.append(owned)
|
||||||
|
contents_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func can_add_item(item_id: StringName, quantity: int = 1) -> bool:
|
func can_add_item(item_id: StringName, quantity: int = 1) -> bool:
|
||||||
var item: ItemDataType = _resolve_available_item(item_id)
|
var item: ItemDataType = _resolve_available_item(item_id)
|
||||||
if item == null or quantity <= 0:
|
if item == null or quantity <= 0:
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,25 @@ func get_fish_slots() -> Array[StringName]:
|
||||||
return _fish_slots.duplicate()
|
return _fish_slots.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func find_item_slot(item_id: StringName) -> int:
|
||||||
|
return _slots.find(item_id)
|
||||||
|
|
||||||
|
|
||||||
|
func find_first_empty_slot() -> int:
|
||||||
|
for slot_index: int in SLOT_COUNT:
|
||||||
|
if get_assignment_kind(slot_index) != AssignmentKind.EMPTY:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
_inventory_layout == null
|
||||||
|
or _inventory_layout.get_key_at(
|
||||||
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||||
|
slot_index,
|
||||||
|
).is_empty()
|
||||||
|
):
|
||||||
|
return slot_index
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
func replace_state(
|
func replace_state(
|
||||||
slots: Array[StringName],
|
slots: Array[StringName],
|
||||||
selected_slot: int,
|
selected_slot: int,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ var _fish_inventory: FishInventoryType
|
||||||
var _item_catalog: ItemCatalogType
|
var _item_catalog: ItemCatalogType
|
||||||
var _storage_capacity: PlayerCoolerCapacityType
|
var _storage_capacity: PlayerCoolerCapacityType
|
||||||
var _placements: Dictionary[String, Dictionary] = {}
|
var _placements: Dictionary[String, Dictionary] = {}
|
||||||
|
var _prepared_item_placements: Dictionary[String, Dictionary] = {}
|
||||||
var _reconciling: bool = false
|
var _reconciling: bool = false
|
||||||
var _backpack_level: int = 0
|
var _backpack_level: int = 0
|
||||||
|
|
||||||
|
|
@ -72,7 +73,44 @@ func can_accept_item(item_id: StringName) -> bool:
|
||||||
if not is_managed_item(item_id):
|
if not is_managed_item(item_id):
|
||||||
return true
|
return true
|
||||||
var key := item_key(item_id)
|
var key := item_key(item_id)
|
||||||
return _placements.has(key) or _first_free_slot(InventoryContainer.INVENTORY) >= 0
|
return (
|
||||||
|
_placements.has(key)
|
||||||
|
or _is_prepared_item_placement_available(key)
|
||||||
|
or _first_free_slot(InventoryContainer.INVENTORY) >= 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func prepare_new_item_placement(
|
||||||
|
item_id: StringName,
|
||||||
|
target_container: InventoryContainer,
|
||||||
|
target_slot: int,
|
||||||
|
) -> bool:
|
||||||
|
if not is_managed_item(item_id):
|
||||||
|
return false
|
||||||
|
var key := item_key(item_id)
|
||||||
|
if (
|
||||||
|
_placements.has(key)
|
||||||
|
or not _is_slot_valid(target_container, target_slot)
|
||||||
|
or not _key_at(target_container, target_slot).is_empty()
|
||||||
|
or _prepared_slot_is_occupied(target_container, target_slot)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_prepared_item_placements[key] = {
|
||||||
|
"container": int(target_container),
|
||||||
|
"slot": target_slot,
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func cancel_prepared_item_placement(item_id: StringName) -> void:
|
||||||
|
_prepared_item_placements.erase(item_key(item_id))
|
||||||
|
|
||||||
|
|
||||||
|
func can_accept_item_in_storage(item_id: StringName) -> bool:
|
||||||
|
if not is_managed_item(item_id):
|
||||||
|
return true
|
||||||
|
var key := item_key(item_id)
|
||||||
|
return _placements.has(key) or _first_free_slot(InventoryContainer.STORAGE) >= 0
|
||||||
|
|
||||||
|
|
||||||
func can_accept_catch(catch_id: StringName = StringName()) -> bool:
|
func can_accept_catch(catch_id: StringName = StringName()) -> bool:
|
||||||
|
|
@ -332,6 +370,7 @@ func restore_from_save_data(data: Dictionary) -> bool:
|
||||||
"container": container,
|
"container": container,
|
||||||
"slot": slot,
|
"slot": slot,
|
||||||
}
|
}
|
||||||
|
_prepared_item_placements.clear()
|
||||||
_placements = restored
|
_placements = restored
|
||||||
_reconcile(true)
|
_reconcile(true)
|
||||||
return true
|
return true
|
||||||
|
|
@ -340,6 +379,7 @@ func restore_from_save_data(data: Dictionary) -> bool:
|
||||||
func reset_to_defaults() -> void:
|
func reset_to_defaults() -> void:
|
||||||
var capacity_changed := _backpack_level != 0
|
var capacity_changed := _backpack_level != 0
|
||||||
_backpack_level = 0
|
_backpack_level = 0
|
||||||
|
_prepared_item_placements.clear()
|
||||||
_placements.clear()
|
_placements.clear()
|
||||||
_reconcile(true)
|
_reconcile(true)
|
||||||
if capacity_changed:
|
if capacity_changed:
|
||||||
|
|
@ -402,8 +442,17 @@ func _reconcile(force_signal: bool) -> void:
|
||||||
for key: String in expected:
|
for key: String in expected:
|
||||||
if _placements.has(key):
|
if _placements.has(key):
|
||||||
continue
|
continue
|
||||||
|
var prepared: Dictionary = _prepared_item_placements.get(key, {})
|
||||||
var target_container := InventoryContainer.INVENTORY
|
var target_container := InventoryContainer.INVENTORY
|
||||||
var target_slot := _first_free_slot(target_container)
|
var target_slot: int = -1
|
||||||
|
if _is_prepared_item_placement_available(key):
|
||||||
|
target_container = int(
|
||||||
|
prepared["container"]
|
||||||
|
) as InventoryContainer
|
||||||
|
target_slot = int(prepared["slot"])
|
||||||
|
_prepared_item_placements.erase(key)
|
||||||
|
if target_slot < 0:
|
||||||
|
target_slot = _first_free_slot(target_container)
|
||||||
if target_slot < 0:
|
if target_slot < 0:
|
||||||
target_container = InventoryContainer.STORAGE
|
target_container = InventoryContainer.STORAGE
|
||||||
target_slot = _first_free_slot(target_container)
|
target_slot = _first_free_slot(target_container)
|
||||||
|
|
@ -484,6 +533,37 @@ func _first_free_slot(container: InventoryContainer) -> int:
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
func _is_prepared_item_placement_available(key: String) -> bool:
|
||||||
|
var placement: Dictionary = _prepared_item_placements.get(key, {})
|
||||||
|
if placement.is_empty():
|
||||||
|
return false
|
||||||
|
var container: int = int(placement.get("container", -1))
|
||||||
|
var slot: int = int(placement.get("slot", -1))
|
||||||
|
if (
|
||||||
|
container not in [
|
||||||
|
InventoryContainer.INVENTORY,
|
||||||
|
InventoryContainer.STORAGE,
|
||||||
|
InventoryContainer.HOTBAR,
|
||||||
|
]
|
||||||
|
or not _is_slot_valid(container as InventoryContainer, slot)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return _key_at(container as InventoryContainer, slot).is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func _prepared_slot_is_occupied(
|
||||||
|
container: InventoryContainer,
|
||||||
|
slot: int,
|
||||||
|
) -> bool:
|
||||||
|
for placement: Dictionary in _prepared_item_placements.values():
|
||||||
|
if (
|
||||||
|
int(placement.get("container", -1)) == int(container)
|
||||||
|
and int(placement.get("slot", -1)) == slot
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _key_at(container: InventoryContainer, slot: int) -> String:
|
func _key_at(container: InventoryContainer, slot: int) -> String:
|
||||||
for key: String in _placements:
|
for key: String in _placements:
|
||||||
var placement: Dictionary = _placements[key]
|
var placement: Dictionary = _placements[key]
|
||||||
|
|
|
||||||
17
items/catalog/basic_cube.tres
Normal file
17
items/catalog/basic_cube.tres
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
|
||||||
|
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="2_icon"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_item")
|
||||||
|
item_id = &"basic_cube"
|
||||||
|
display_name = "basic cube"
|
||||||
|
description = "a simple piece of decor for testing your RV layout."
|
||||||
|
category = 6
|
||||||
|
icon = ExtResource("2_icon")
|
||||||
|
stackable = true
|
||||||
|
max_stack = 32
|
||||||
|
usable = false
|
||||||
|
equippable = true
|
||||||
|
hotbar_allowed = true
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=41 format=3]
|
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=42 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"]
|
||||||
|
|
@ -40,7 +40,8 @@
|
||||||
[ext_resource type="Resource" path="res://items/catalog/crab_net.tres" id="39_crab_net"]
|
[ext_resource type="Resource" path="res://items/catalog/crab_net.tres" id="39_crab_net"]
|
||||||
[ext_resource type="Resource" path="res://items/catalog/standard_shovel.tres" id="40_shovel"]
|
[ext_resource type="Resource" path="res://items/catalog/standard_shovel.tres" id="40_shovel"]
|
||||||
[ext_resource type="Resource" path="res://items/catalog/fishing_net.tres" id="41_fishing_net"]
|
[ext_resource type="Resource" path="res://items/catalog/fishing_net.tres" id="41_fishing_net"]
|
||||||
|
[ext_resource type="Resource" path="res://items/catalog/basic_cube.tres" id="42_basic_cube"]
|
||||||
|
|
||||||
[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("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet"), ExtResource("39_crab_net"), ExtResource("40_shovel"), ExtResource("41_fishing_net")]
|
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet"), ExtResource("39_crab_net"), ExtResource("40_shovel"), ExtResource("41_fishing_net"), ExtResource("42_basic_cube")]
|
||||||
|
|
|
||||||
317
main/main.gd
317
main/main.gd
|
|
@ -37,6 +37,9 @@ const FishingShopType = preload("res://ui/fishing_shop.gd")
|
||||||
const FishingShopInteractionType = preload(
|
const FishingShopInteractionType = preload(
|
||||||
"res://world/fishing_shop_interaction.gd"
|
"res://world/fishing_shop_interaction.gd"
|
||||||
)
|
)
|
||||||
|
const DecorShopInteractionType = preload(
|
||||||
|
"res://world/decor_shop_interaction.gd"
|
||||||
|
)
|
||||||
const PlayerStorageInteractionType = preload(
|
const PlayerStorageInteractionType = preload(
|
||||||
"res://world/player_storage_interaction.gd"
|
"res://world/player_storage_interaction.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -70,6 +73,7 @@ const NetworkFishingServiceType = preload(
|
||||||
"res://network/network_fishing_service.gd"
|
"res://network/network_fishing_service.gd"
|
||||||
)
|
)
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||||
const NetworkSaleServiceType = preload(
|
const NetworkSaleServiceType = preload(
|
||||||
"res://network/network_sale_service.gd"
|
"res://network/network_sale_service.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -123,6 +127,14 @@ const GatheringControllerType = preload(
|
||||||
const NetworkWorldSpawnServiceType = preload(
|
const NetworkWorldSpawnServiceType = preload(
|
||||||
"res://network/network_world_spawn_service.gd"
|
"res://network/network_world_spawn_service.gd"
|
||||||
)
|
)
|
||||||
|
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
|
||||||
|
const HomeWorldServiceType = preload("res://homes/home_world_service.gd")
|
||||||
|
const NetworkHomeServiceType = preload(
|
||||||
|
"res://network/network_home_service.gd"
|
||||||
|
)
|
||||||
|
const HomeDecorControllerType = preload(
|
||||||
|
"res://homes/home_decor_controller.gd"
|
||||||
|
)
|
||||||
|
|
||||||
const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||||
const TITLE_MUSIC_PATH: String = (
|
const TITLE_MUSIC_PATH: String = (
|
||||||
|
|
@ -153,6 +165,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
|
||||||
@onready var _game_ui: GameUIType = %GameUI
|
@onready var _game_ui: GameUIType = %GameUI
|
||||||
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
|
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
|
||||||
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
|
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
|
||||||
|
@onready var _home_state: PlayerHomeStateType = %PlayerHomeState
|
||||||
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
|
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
|
||||||
@onready var _controller_mapping_manager: ControllerMappingManagerType = (
|
@onready var _controller_mapping_manager: ControllerMappingManagerType = (
|
||||||
%ControllerMappingManager
|
%ControllerMappingManager
|
||||||
|
|
@ -212,6 +225,10 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
|
||||||
)
|
)
|
||||||
@onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService
|
@onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService
|
||||||
@onready var _network_shop: NetworkShopServiceType = %NetworkShopService
|
@onready var _network_shop: NetworkShopServiceType = %NetworkShopService
|
||||||
|
@onready var _network_home: NetworkHomeServiceType = %NetworkHomeService
|
||||||
|
@onready var _home_decor_controller: HomeDecorControllerType = (
|
||||||
|
%HomeDecorController
|
||||||
|
)
|
||||||
@onready var _network_item_use: NetworkItemUseServiceType = (
|
@onready var _network_item_use: NetworkItemUseServiceType = (
|
||||||
%NetworkItemUseService
|
%NetworkItemUseService
|
||||||
)
|
)
|
||||||
|
|
@ -234,6 +251,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
|
||||||
@onready var _players_root: Node3D = $Players
|
@onready var _players_root: Node3D = $Players
|
||||||
@onready var _surface_drawings_root: Node3D = $SurfaceDrawings
|
@onready var _surface_drawings_root: Node3D = $SurfaceDrawings
|
||||||
@onready var _world_gatherables_root: Node3D = $WorldGatherables
|
@onready var _world_gatherables_root: Node3D = $WorldGatherables
|
||||||
|
@onready var _home_world: HomeWorldServiceType = %HomeWorldService
|
||||||
@onready var _title_background: ColorRect = %TitleBackground
|
@onready var _title_background: ColorRect = %TitleBackground
|
||||||
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
|
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
|
||||||
@onready var _shop_backdrop: ColorRect = %ShopBackdrop
|
@onready var _shop_backdrop: ColorRect = %ShopBackdrop
|
||||||
|
|
@ -241,6 +259,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
|
||||||
var _gameplay_started: bool = false
|
var _gameplay_started: bool = false
|
||||||
var _save_slots := PlayerSaveSlotCatalogType.new()
|
var _save_slots := PlayerSaveSlotCatalogType.new()
|
||||||
var _shop_interaction: FishingShopInteractionType
|
var _shop_interaction: FishingShopInteractionType
|
||||||
|
var _decor_shop_interaction: DecorShopInteractionType
|
||||||
var _storage_interaction: PlayerStorageInteractionType
|
var _storage_interaction: PlayerStorageInteractionType
|
||||||
var _title_music_tween: Tween
|
var _title_music_tween: Tween
|
||||||
var _title_music_transition_generation: int = 0
|
var _title_music_transition_generation: int = 0
|
||||||
|
|
@ -740,6 +759,7 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_player.inventory_layout,
|
_player.inventory_layout,
|
||||||
)
|
)
|
||||||
_shop_interaction = _test_world.get_fishing_shop()
|
_shop_interaction = _test_world.get_fishing_shop()
|
||||||
|
_decor_shop_interaction = _test_world.get_decor_shop()
|
||||||
_storage_interaction = _test_world.get_player_storage()
|
_storage_interaction = _test_world.get_player_storage()
|
||||||
if not dedicated:
|
if not dedicated:
|
||||||
_shop_interaction.setup_local_player(_player)
|
_shop_interaction.setup_local_player(_player)
|
||||||
|
|
@ -749,6 +769,10 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_game_ui.set_shop_npc_player_in_range(
|
_game_ui.set_shop_npc_player_in_range(
|
||||||
_shop_interaction.is_local_player_in_range()
|
_shop_interaction.is_local_player_in_range()
|
||||||
)
|
)
|
||||||
|
_decor_shop_interaction.setup_local_player(_player)
|
||||||
|
_decor_shop_interaction.local_player_range_changed.connect(
|
||||||
|
_on_decor_shop_range_changed
|
||||||
|
)
|
||||||
_storage_interaction.setup_local_player(_player)
|
_storage_interaction.setup_local_player(_player)
|
||||||
_storage_interaction.local_player_range_changed.connect(
|
_storage_interaction.local_player_range_changed.connect(
|
||||||
_on_storage_range_changed
|
_on_storage_range_changed
|
||||||
|
|
@ -770,6 +794,23 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_world_weather,
|
_world_weather,
|
||||||
_player_jobs,
|
_player_jobs,
|
||||||
_player,
|
_player,
|
||||||
|
_home_state,
|
||||||
|
)
|
||||||
|
_home_world.setup(
|
||||||
|
_test_world,
|
||||||
|
_player_spawn_service,
|
||||||
|
_network_session,
|
||||||
|
dedicated,
|
||||||
|
)
|
||||||
|
_network_home.setup(
|
||||||
|
_network_session,
|
||||||
|
_player_spawn_service,
|
||||||
|
_home_state,
|
||||||
|
_save_manager,
|
||||||
|
_home_world,
|
||||||
|
_player.bag,
|
||||||
|
_player.inventory_layout,
|
||||||
|
_player.hotbar,
|
||||||
)
|
)
|
||||||
_player_jobs.set_save_manager(_save_manager)
|
_player_jobs.set_save_manager(_save_manager)
|
||||||
_save_manager.set_autosave_enabled(false)
|
_save_manager.set_autosave_enabled(false)
|
||||||
|
|
@ -903,6 +944,9 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_save_manager,
|
_save_manager,
|
||||||
_asset_reservations,
|
_asset_reservations,
|
||||||
_player.inventory_layout,
|
_player.inventory_layout,
|
||||||
|
_home_state,
|
||||||
|
_network_home,
|
||||||
|
_decor_shop_interaction,
|
||||||
)
|
)
|
||||||
_fishing_spot.setup(
|
_fishing_spot.setup(
|
||||||
_player,
|
_player,
|
||||||
|
|
@ -920,6 +964,7 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_network_fishing,
|
_network_fishing,
|
||||||
_world_time,
|
_world_time,
|
||||||
_world_weather,
|
_world_weather,
|
||||||
|
Callable(_test_world, "get_fishable_water_regions"),
|
||||||
)
|
)
|
||||||
if dedicated:
|
if dedicated:
|
||||||
return
|
return
|
||||||
|
|
@ -941,11 +986,13 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_player.fishing_upgrades,
|
_player.fishing_upgrades,
|
||||||
_shop_interaction,
|
_shop_interaction,
|
||||||
_storage_interaction,
|
_storage_interaction,
|
||||||
|
_decor_shop_interaction,
|
||||||
_player.item_effects,
|
_player.item_effects,
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
_network_session,
|
_network_session,
|
||||||
_network_sale,
|
_network_sale,
|
||||||
_network_shop,
|
_network_shop,
|
||||||
|
_home_state,
|
||||||
_network_chat,
|
_network_chat,
|
||||||
_network_profile,
|
_network_profile,
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
|
|
@ -963,6 +1010,21 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_test_world.get_world_environment(),
|
_test_world.get_world_environment(),
|
||||||
_test_world.get_sun(),
|
_test_world.get_sun(),
|
||||||
)
|
)
|
||||||
|
_home_decor_controller.setup(
|
||||||
|
_player,
|
||||||
|
_player.hotbar,
|
||||||
|
_player.bag,
|
||||||
|
_home_state,
|
||||||
|
_network_home,
|
||||||
|
_home_world,
|
||||||
|
_fishing_spot,
|
||||||
|
_game_ui.get_home_decor_toolbar(),
|
||||||
|
)
|
||||||
|
_game_ui.get_player_storage().decorate_requested.connect(
|
||||||
|
_home_decor_controller.begin_decorating
|
||||||
|
)
|
||||||
|
_network_home.local_space_changed.connect(_on_local_home_space_changed)
|
||||||
|
_game_ui.rv_storage_requested.connect(_on_rv_storage_requested)
|
||||||
_game_ui.setup_data_and_identity(
|
_game_ui.setup_data_and_identity(
|
||||||
_data_root,
|
_data_root,
|
||||||
_identity_backups,
|
_identity_backups,
|
||||||
|
|
@ -1006,7 +1068,8 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_game_ui,
|
_game_ui,
|
||||||
_game_ui.get_screen_fade(),
|
_game_ui.get_screen_fade(),
|
||||||
_test_world.get_player_water_triggers(),
|
_test_world.get_player_water_triggers(),
|
||||||
_test_world.get_safe_respawn_points()
|
_test_world.get_safe_respawn_points(),
|
||||||
|
Callable(_test_world, "get_water_recovery_position"),
|
||||||
)
|
)
|
||||||
_ui_pixelation.effective_pixel_size_changed.connect(
|
_ui_pixelation.effective_pixel_size_changed.connect(
|
||||||
_on_effective_ui_pixel_size_changed
|
_on_effective_ui_pixel_size_changed
|
||||||
|
|
@ -1473,6 +1536,9 @@ func _input(event: InputEvent) -> void:
|
||||||
if _game_ui.get_player_storage().consume_escape():
|
if _game_ui.get_player_storage().consume_escape():
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
if _game_ui.get_decor_shop().consume_escape():
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if fishing_shop.consume_escape():
|
if fishing_shop.consume_escape():
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
@ -1571,19 +1637,50 @@ func _is_pause_open_request(event: InputEvent) -> bool:
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if (
|
||||||
|
_gameplay_started
|
||||||
|
and event.is_action_pressed("fish_primary")
|
||||||
|
and not (event is InputEventKey and event.echo)
|
||||||
|
and _fishing_spot.try_use_active_non_fishing_item()
|
||||||
|
):
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if (
|
if (
|
||||||
not _gameplay_started
|
not _gameplay_started
|
||||||
or not event.is_action_pressed("interact")
|
or not event.is_action_pressed("interact")
|
||||||
or (event is InputEventKey and event.echo)
|
or (event is InputEventKey and event.echo)
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
var home_owner: String = _network_home.get_local_home_owner_fingerprint()
|
||||||
|
if not home_owner.is_empty():
|
||||||
|
if _home_world.is_avatar_near_interior_exit(_player, home_owner):
|
||||||
|
if _network_home.request_exit():
|
||||||
|
_game_ui.set_storage_prompt_visible(false)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
_network_home.can_local_decorate()
|
||||||
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
|
and _game_ui.get_player_storage().open_storage_from_home(true)
|
||||||
|
):
|
||||||
|
_game_ui.set_storage_prompt_visible(false)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
var nearby_home_owner: String = _home_world.find_nearby_exterior_owner(
|
||||||
|
_player
|
||||||
|
)
|
||||||
|
if not nearby_home_owner.is_empty():
|
||||||
|
if _network_home.request_enter(nearby_home_owner):
|
||||||
|
_game_ui.set_storage_prompt_visible(false)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if (
|
if (
|
||||||
_storage_interaction != null
|
_decor_shop_interaction != null
|
||||||
and _storage_interaction.is_local_player_in_range()
|
and _decor_shop_interaction.is_local_player_in_range()
|
||||||
and _fishing_spot.can_open_fishing_shop()
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
and _game_ui.get_player_storage().open_storage()
|
and _game_ui.get_decor_shop().open_shop()
|
||||||
):
|
):
|
||||||
_game_ui.set_storage_prompt_visible(false)
|
_game_ui.set_shop_prompt_visible(false)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
if (
|
if (
|
||||||
|
|
@ -1606,37 +1703,42 @@ func _process(delta: float) -> void:
|
||||||
)
|
)
|
||||||
_print_dedicated_metrics()
|
_print_dedicated_metrics()
|
||||||
return
|
return
|
||||||
var show_shop_prompt := _can_show_shop_prompt()
|
var show_decor_shop_prompt := _can_show_decor_shop_prompt()
|
||||||
|
var show_shop_prompt := show_decor_shop_prompt or _can_show_shop_prompt()
|
||||||
var shop_prompt_anchor := (
|
var shop_prompt_anchor := (
|
||||||
_shop_interaction.get_prompt_anchor_position()
|
_decor_shop_interaction.get_prompt_anchor_position()
|
||||||
if _shop_interaction != null
|
if show_decor_shop_prompt
|
||||||
else Vector3.ZERO
|
else (
|
||||||
|
_shop_interaction.get_prompt_anchor_position()
|
||||||
|
if _shop_interaction != null else Vector3.ZERO
|
||||||
|
)
|
||||||
)
|
)
|
||||||
_game_ui.set_shop_prompt_visible(
|
_game_ui.set_shop_prompt_visible(
|
||||||
show_shop_prompt,
|
show_shop_prompt,
|
||||||
shop_prompt_anchor,
|
shop_prompt_anchor,
|
||||||
)
|
)
|
||||||
var show_storage_prompt := _can_show_storage_prompt()
|
var home_prompt := _get_home_prompt()
|
||||||
var storage_prompt_anchor := (
|
|
||||||
_storage_interaction.get_prompt_anchor_position()
|
|
||||||
if _storage_interaction != null
|
|
||||||
else Vector3.ZERO
|
|
||||||
)
|
|
||||||
_game_ui.set_storage_prompt_visible(
|
_game_ui.set_storage_prompt_visible(
|
||||||
show_storage_prompt,
|
not home_prompt.is_empty(),
|
||||||
storage_prompt_anchor,
|
home_prompt.get("anchor", Vector3.ZERO),
|
||||||
|
str(home_prompt.get("label", "enter RV")),
|
||||||
|
)
|
||||||
|
_game_ui.set_rv_storage_button_visible(
|
||||||
|
_can_show_rv_storage_button()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||||
if settings == null:
|
if settings == null:
|
||||||
return
|
return
|
||||||
var requested_window_mode: DisplayServer.WindowMode = (
|
var current_window_mode: DisplayServer.WindowMode = (
|
||||||
DisplayServer.WINDOW_MODE_FULLSCREEN
|
DisplayServer.window_get_mode()
|
||||||
if settings.fullscreen_enabled
|
|
||||||
else DisplayServer.WINDOW_MODE_WINDOWED
|
|
||||||
)
|
)
|
||||||
if DisplayServer.window_get_mode() != requested_window_mode:
|
var requested_window_mode: DisplayServer.WindowMode = resolve_window_mode(
|
||||||
|
settings.fullscreen_enabled,
|
||||||
|
current_window_mode,
|
||||||
|
)
|
||||||
|
if current_window_mode != requested_window_mode:
|
||||||
DisplayServer.window_set_mode(requested_window_mode)
|
DisplayServer.window_set_mode(requested_window_mode)
|
||||||
_apply_world_pixelation(settings.world_pixel_size)
|
_apply_world_pixelation(settings.world_pixel_size)
|
||||||
_ui_pixelation.set_pixel_size(settings.ui_pixel_size)
|
_ui_pixelation.set_pixel_size(settings.ui_pixel_size)
|
||||||
|
|
@ -1663,6 +1765,25 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func resolve_window_mode(
|
||||||
|
fullscreen_enabled: bool,
|
||||||
|
current_mode: DisplayServer.WindowMode,
|
||||||
|
) -> DisplayServer.WindowMode:
|
||||||
|
var is_fullscreen: bool = current_mode in [
|
||||||
|
DisplayServer.WINDOW_MODE_FULLSCREEN,
|
||||||
|
DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN,
|
||||||
|
]
|
||||||
|
if fullscreen_enabled:
|
||||||
|
return (
|
||||||
|
current_mode
|
||||||
|
if is_fullscreen
|
||||||
|
else DisplayServer.WINDOW_MODE_FULLSCREEN
|
||||||
|
)
|
||||||
|
if is_fullscreen:
|
||||||
|
return DisplayServer.WINDOW_MODE_WINDOWED
|
||||||
|
return current_mode
|
||||||
|
|
||||||
|
|
||||||
func _apply_world_pixelation(pixel_size: int) -> void:
|
func _apply_world_pixelation(pixel_size: int) -> void:
|
||||||
var root_viewport: Viewport = get_viewport()
|
var root_viewport: Viewport = get_viewport()
|
||||||
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
|
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
|
||||||
|
|
@ -2344,17 +2465,9 @@ func _on_remote_recovery_requested(
|
||||||
peer_id,
|
peer_id,
|
||||||
"Fishing attempt ended."
|
"Fishing attempt ended."
|
||||||
)
|
)
|
||||||
var target_position: Vector3 = _test_world.get_player_spawn_transform().origin
|
var target_position := _test_world.get_water_recovery_position(
|
||||||
var nearest_distance: float = INF
|
entry_position
|
||||||
for point: SafeRespawnPoint in _test_world.get_safe_respawn_points():
|
)
|
||||||
if point == null or not point.enabled:
|
|
||||||
continue
|
|
||||||
var distance: float = point.get_horizontal_distance_squared(
|
|
||||||
entry_position
|
|
||||||
)
|
|
||||||
if distance < nearest_distance:
|
|
||||||
nearest_distance = distance
|
|
||||||
target_position = point.global_position
|
|
||||||
target_position.y += _water_recovery.respawn_height_offset
|
target_position.y += _water_recovery.respawn_height_offset
|
||||||
avatar.global_position = target_position
|
avatar.global_position = target_position
|
||||||
avatar.velocity = Vector3.ZERO
|
avatar.velocity = Vector3.ZERO
|
||||||
|
|
@ -2427,11 +2540,13 @@ func _apply_world(
|
||||||
if _application_initialized:
|
if _application_initialized:
|
||||||
_refresh_active_world_bindings()
|
_refresh_active_world_bindings()
|
||||||
_network_world_spawns.refresh_world_context()
|
_network_world_spawns.refresh_world_context()
|
||||||
|
_network_home.refresh_world_presentations()
|
||||||
if _application_initialized and not _dedicated_runtime:
|
if _application_initialized and not _dedicated_runtime:
|
||||||
_water_recovery.update_world_context(
|
_water_recovery.update_world_context(
|
||||||
spawn_transform,
|
spawn_transform,
|
||||||
_test_world.get_player_water_triggers(),
|
_test_world.get_player_water_triggers(),
|
||||||
_test_world.get_safe_respawn_points(),
|
_test_world.get_safe_respawn_points(),
|
||||||
|
Callable(_test_world, "get_water_recovery_position"),
|
||||||
)
|
)
|
||||||
_shoreline_ambience.configure(
|
_shoreline_ambience.configure(
|
||||||
_player,
|
_player,
|
||||||
|
|
@ -2455,10 +2570,19 @@ func _refresh_active_world_bindings() -> void:
|
||||||
_storage_interaction.local_player_range_changed.disconnect(
|
_storage_interaction.local_player_range_changed.disconnect(
|
||||||
_on_storage_range_changed
|
_on_storage_range_changed
|
||||||
)
|
)
|
||||||
|
if is_instance_valid(_decor_shop_interaction):
|
||||||
|
if _decor_shop_interaction.local_player_range_changed.is_connected(
|
||||||
|
_on_decor_shop_range_changed
|
||||||
|
):
|
||||||
|
_decor_shop_interaction.local_player_range_changed.disconnect(
|
||||||
|
_on_decor_shop_range_changed
|
||||||
|
)
|
||||||
_shop_interaction = _test_world.get_fishing_shop()
|
_shop_interaction = _test_world.get_fishing_shop()
|
||||||
|
_decor_shop_interaction = _test_world.get_decor_shop()
|
||||||
_storage_interaction = _test_world.get_player_storage()
|
_storage_interaction = _test_world.get_player_storage()
|
||||||
_network_sale.set_shop_interaction(_shop_interaction)
|
_network_sale.set_shop_interaction(_shop_interaction)
|
||||||
_network_shop.set_shop_interaction(_shop_interaction)
|
_network_shop.set_shop_interaction(_shop_interaction)
|
||||||
|
_network_shop.set_decor_shop_interaction(_decor_shop_interaction)
|
||||||
if _dedicated_runtime:
|
if _dedicated_runtime:
|
||||||
return
|
return
|
||||||
if _shop_interaction != null:
|
if _shop_interaction != null:
|
||||||
|
|
@ -2471,9 +2595,15 @@ func _refresh_active_world_bindings() -> void:
|
||||||
_storage_interaction.local_player_range_changed.connect(
|
_storage_interaction.local_player_range_changed.connect(
|
||||||
_on_storage_range_changed
|
_on_storage_range_changed
|
||||||
)
|
)
|
||||||
|
if _decor_shop_interaction != null:
|
||||||
|
_decor_shop_interaction.setup_local_player(_player)
|
||||||
|
_decor_shop_interaction.local_player_range_changed.connect(
|
||||||
|
_on_decor_shop_range_changed
|
||||||
|
)
|
||||||
_game_ui.set_world_interactions(
|
_game_ui.set_world_interactions(
|
||||||
_shop_interaction,
|
_shop_interaction,
|
||||||
_storage_interaction,
|
_storage_interaction,
|
||||||
|
_decor_shop_interaction,
|
||||||
)
|
)
|
||||||
_on_shop_range_changed(
|
_on_shop_range_changed(
|
||||||
_shop_interaction != null
|
_shop_interaction != null
|
||||||
|
|
@ -2483,6 +2613,10 @@ func _refresh_active_world_bindings() -> void:
|
||||||
_storage_interaction != null
|
_storage_interaction != null
|
||||||
and _storage_interaction.is_local_player_in_range()
|
and _storage_interaction.is_local_player_in_range()
|
||||||
)
|
)
|
||||||
|
_on_decor_shop_range_changed(
|
||||||
|
_decor_shop_interaction != null
|
||||||
|
and _decor_shop_interaction.is_local_player_in_range()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _on_water_recovery_starting() -> void:
|
func _on_water_recovery_starting() -> void:
|
||||||
|
|
@ -2496,6 +2630,7 @@ func _on_water_recovery_starting() -> void:
|
||||||
_game_ui.close_player_menu_for_water_recovery()
|
_game_ui.close_player_menu_for_water_recovery()
|
||||||
_game_ui.get_pause_menu().close_for_water_recovery()
|
_game_ui.get_pause_menu().close_for_water_recovery()
|
||||||
_game_ui.get_fishing_shop().close_for_water_recovery()
|
_game_ui.get_fishing_shop().close_for_water_recovery()
|
||||||
|
_game_ui.get_decor_shop().close_for_water_recovery()
|
||||||
_game_ui.get_player_storage().close_for_water_recovery()
|
_game_ui.get_player_storage().close_for_water_recovery()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2557,6 +2692,12 @@ func _on_active_hotbar_item_changed(
|
||||||
and item.is_available()
|
and item.is_available()
|
||||||
and _player.bag.owns_item(item_id)
|
and _player.bag.owns_item(item_id)
|
||||||
)
|
)
|
||||||
|
var active_is_decor: bool = (
|
||||||
|
DecorCatalogType.has_product(item_id)
|
||||||
|
and item != null
|
||||||
|
and item.is_available()
|
||||||
|
and _player.bag.owns_item(item_id)
|
||||||
|
)
|
||||||
_player.set_active_fishing_rod(
|
_player.set_active_fishing_rod(
|
||||||
item as FishingRodDataType if active_is_rod else null,
|
item as FishingRodDataType if active_is_rod else null,
|
||||||
true,
|
true,
|
||||||
|
|
@ -2567,6 +2708,7 @@ func _on_active_hotbar_item_changed(
|
||||||
)
|
)
|
||||||
_player.set_active_catching_net(active_is_catching_net)
|
_player.set_active_catching_net(active_is_catching_net)
|
||||||
_player.set_active_shovel(active_is_shovel)
|
_player.set_active_shovel(active_is_shovel)
|
||||||
|
_player.set_active_decor_package(active_is_decor)
|
||||||
_game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit)
|
_game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit)
|
||||||
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
|
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
|
||||||
item != null and _player.bag.owns_item(item_id)
|
item != null and _player.bag.owns_item(item_id)
|
||||||
|
|
@ -2703,7 +2845,13 @@ func _exit_tree() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_shop_range_changed(in_range: bool) -> void:
|
func _on_shop_range_changed(in_range: bool) -> void:
|
||||||
_game_ui.set_shop_npc_player_in_range(in_range)
|
_game_ui.set_shop_npc_player_in_range(
|
||||||
|
in_range
|
||||||
|
or (
|
||||||
|
_decor_shop_interaction != null
|
||||||
|
and _decor_shop_interaction.is_local_player_in_range()
|
||||||
|
)
|
||||||
|
)
|
||||||
if not in_range:
|
if not in_range:
|
||||||
_game_ui.get_fishing_shop().close_for_range_exit()
|
_game_ui.get_fishing_shop().close_for_range_exit()
|
||||||
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
||||||
|
|
@ -2712,7 +2860,50 @@ func _on_shop_range_changed(in_range: bool) -> void:
|
||||||
func _on_storage_range_changed(in_range: bool) -> void:
|
func _on_storage_range_changed(in_range: bool) -> void:
|
||||||
if not in_range:
|
if not in_range:
|
||||||
_game_ui.get_player_storage().close_for_range_exit()
|
_game_ui.get_player_storage().close_for_range_exit()
|
||||||
_game_ui.set_storage_prompt_visible(_can_show_storage_prompt())
|
var prompt := _get_home_prompt()
|
||||||
|
_game_ui.set_storage_prompt_visible(
|
||||||
|
not prompt.is_empty(),
|
||||||
|
prompt.get("anchor", Vector3.ZERO),
|
||||||
|
str(prompt.get("label", "enter RV")),
|
||||||
|
)
|
||||||
|
_game_ui.set_rv_storage_button_visible(
|
||||||
|
_can_show_rv_storage_button()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_decor_shop_range_changed(in_range: bool) -> void:
|
||||||
|
_game_ui.set_shop_npc_player_in_range(
|
||||||
|
in_range
|
||||||
|
or (
|
||||||
|
_shop_interaction != null
|
||||||
|
and _shop_interaction.is_local_player_in_range()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not in_range:
|
||||||
|
_game_ui.get_decor_shop().close_for_range_exit()
|
||||||
|
_game_ui.set_shop_prompt_visible(
|
||||||
|
_can_show_shop_prompt() or _can_show_decor_shop_prompt()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_local_home_space_changed(
|
||||||
|
space_id: StringName,
|
||||||
|
_owner_fingerprint: String,
|
||||||
|
) -> void:
|
||||||
|
var inside_rv: bool = String(space_id).begins_with("rv:")
|
||||||
|
_fishing_spot.set_fishing_environment_enabled(not inside_rv)
|
||||||
|
_game_ui.set_rv_interior_camera_mode(
|
||||||
|
inside_rv
|
||||||
|
)
|
||||||
|
_game_ui.set_rv_storage_button_visible(
|
||||||
|
_can_show_rv_storage_button()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_rv_storage_requested() -> void:
|
||||||
|
if not _can_show_rv_storage_button():
|
||||||
|
return
|
||||||
|
_game_ui.get_player_storage().open_storage_from_home(true)
|
||||||
|
|
||||||
|
|
||||||
func _can_show_shop_prompt() -> bool:
|
func _can_show_shop_prompt() -> bool:
|
||||||
|
|
@ -2721,18 +2912,66 @@ func _can_show_shop_prompt() -> bool:
|
||||||
and _shop_interaction != null
|
and _shop_interaction != null
|
||||||
and _shop_interaction.is_local_player_in_range()
|
and _shop_interaction.is_local_player_in_range()
|
||||||
and not _game_ui.get_fishing_shop().visible
|
and not _game_ui.get_fishing_shop().visible
|
||||||
|
and not _game_ui.get_decor_shop().visible
|
||||||
|
and not _water_recovery.is_recovery_active()
|
||||||
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _can_show_decor_shop_prompt() -> bool:
|
||||||
|
return (
|
||||||
|
_gameplay_started
|
||||||
|
and _decor_shop_interaction != null
|
||||||
|
and _decor_shop_interaction.is_local_player_in_range()
|
||||||
|
and not _game_ui.get_decor_shop().visible
|
||||||
|
and not _game_ui.get_fishing_shop().visible
|
||||||
and not _water_recovery.is_recovery_active()
|
and not _water_recovery.is_recovery_active()
|
||||||
and _fishing_spot.can_open_fishing_shop()
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _can_show_storage_prompt() -> bool:
|
func _can_show_storage_prompt() -> bool:
|
||||||
|
return not _get_home_prompt().is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func _can_show_rv_storage_button() -> bool:
|
||||||
return (
|
return (
|
||||||
_gameplay_started
|
_gameplay_started
|
||||||
and _storage_interaction != null
|
and not _network_home.get_local_home_owner_fingerprint().is_empty()
|
||||||
and _storage_interaction.is_local_player_in_range()
|
and _network_home.can_local_decorate()
|
||||||
and not _game_ui.get_player_storage().visible
|
|
||||||
and not _game_ui.get_fishing_shop().visible
|
|
||||||
and not _water_recovery.is_recovery_active()
|
and not _water_recovery.is_recovery_active()
|
||||||
and _fishing_spot.can_open_fishing_shop()
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_home_prompt() -> Dictionary:
|
||||||
|
if (
|
||||||
|
not _gameplay_started
|
||||||
|
or _game_ui.get_player_storage().visible
|
||||||
|
or _game_ui.get_fishing_shop().visible
|
||||||
|
or _game_ui.get_decor_shop().visible
|
||||||
|
or _water_recovery.is_recovery_active()
|
||||||
|
or not _fishing_spot.can_open_fishing_shop()
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var owner_fingerprint: String = (
|
||||||
|
_network_home.get_local_home_owner_fingerprint()
|
||||||
|
)
|
||||||
|
if not owner_fingerprint.is_empty():
|
||||||
|
if _home_world.is_avatar_near_interior_exit(
|
||||||
|
_player, owner_fingerprint
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"label": "leave RV",
|
||||||
|
"anchor": _home_world.get_interior_prompt_position(
|
||||||
|
owner_fingerprint
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
var nearby_owner: String = _home_world.find_nearby_exterior_owner(_player)
|
||||||
|
if nearby_owner.is_empty():
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"label": "enter RV",
|
||||||
|
"anchor": _home_world.get_exterior_prompt_position(nearby_owner),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@
|
||||||
[ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"]
|
[ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"]
|
||||||
[ext_resource type="Script" path="res://network/network_world_spawn_service.gd" id="59_world_spawns"]
|
[ext_resource type="Script" path="res://network/network_world_spawn_service.gd" id="59_world_spawns"]
|
||||||
[ext_resource type="Script" path="res://gathering/gathering_controller.gd" id="60_gathering_controller"]
|
[ext_resource type="Script" path="res://gathering/gathering_controller.gd" id="60_gathering_controller"]
|
||||||
|
[ext_resource type="Script" path="res://homes/player_home_state.gd" id="61_home_state"]
|
||||||
|
[ext_resource type="Script" path="res://homes/home_world_service.gd" id="62_home_world"]
|
||||||
|
[ext_resource type="Script" path="res://network/network_home_service.gd" id="63_network_home"]
|
||||||
|
[ext_resource type="Script" path="res://homes/home_decor_controller.gd" id="64_home_decor_controller"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_grass_native"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_grass_native"]
|
||||||
shader = ExtResource("42_menu_pattern")
|
shader = ExtResource("42_menu_pattern")
|
||||||
|
|
@ -259,6 +263,14 @@ script = ExtResource("22_network_sale")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("23_network_shop")
|
script = ExtResource("23_network_shop")
|
||||||
|
|
||||||
|
[node name="NetworkHomeService" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("63_network_home")
|
||||||
|
|
||||||
|
[node name="HomeDecorController" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("64_home_decor_controller")
|
||||||
|
|
||||||
[node name="NetworkItemUseService" type="Node" parent="." unique_id=255154670]
|
[node name="NetworkItemUseService" type="Node" parent="." unique_id=255154670]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("24_network_item")
|
script = ExtResource("24_network_item")
|
||||||
|
|
@ -322,6 +334,10 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.3999481, 12.07081)
|
||||||
|
|
||||||
[node name="WorldGatherables" type="Node3D" parent="."]
|
[node name="WorldGatherables" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="HomeWorldService" type="Node3D" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("62_home_world")
|
||||||
|
|
||||||
[node name="GatheringController" type="Node3D" parent="."]
|
[node name="GatheringController" type="Node3D" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("60_gathering_controller")
|
script = ExtResource("60_gathering_controller")
|
||||||
|
|
@ -338,6 +354,10 @@ script = ExtResource("8_recovery")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("9_save")
|
script = ExtResource("9_save")
|
||||||
|
|
||||||
|
[node name="PlayerHomeState" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("61_home_state")
|
||||||
|
|
||||||
[node name="PlayerSettingsManager" type="Node" parent="." unique_id=906991991]
|
[node name="PlayerSettingsManager" type="Node" parent="." unique_id=906991991]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("10_settings")
|
script = ExtResource("10_settings")
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,12 @@ func _enter_tree() -> void:
|
||||||
func _on_node_added(node: Node) -> void:
|
func _on_node_added(node: Node) -> void:
|
||||||
enforce_node(node)
|
enforce_node(node)
|
||||||
# Some runtime presenters assign their mesh or material immediately after
|
# Some runtime presenters assign their mesh or material immediately after
|
||||||
# add_child(). Recheck once deferred so those resources are covered too.
|
# add_child(). Recheck those 3D resource owners once deferred so the newly
|
||||||
call_deferred("_enforce_deferred_node", weakref(node))
|
# assigned resources are covered too. CanvasItems keep their filter setting
|
||||||
|
# when their texture changes, so deferring every dynamic UI node only floods
|
||||||
|
# the message queue during large inventory and catalog refreshes.
|
||||||
|
if _needs_deferred_resource_check(node):
|
||||||
|
call_deferred("_enforce_deferred_node", weakref(node))
|
||||||
|
|
||||||
|
|
||||||
func _enforce_existing_tree() -> void:
|
func _enforce_existing_tree() -> void:
|
||||||
|
|
@ -29,6 +33,15 @@ func _enforce_deferred_node(node_ref: WeakRef) -> void:
|
||||||
enforce_node(node)
|
enforce_node(node)
|
||||||
|
|
||||||
|
|
||||||
|
static func _needs_deferred_resource_check(node: Node) -> bool:
|
||||||
|
return (
|
||||||
|
node is GeometryInstance3D
|
||||||
|
or node is MultiMeshInstance3D
|
||||||
|
or node is GPUParticles3D
|
||||||
|
or node is CPUParticles3D
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
static func enforce_subtree(node: Node) -> void:
|
static func enforce_subtree(node: Node) -> void:
|
||||||
enforce_node(node)
|
enforce_node(node)
|
||||||
for child: Node in node.get_children():
|
for child: Node in node.get_children():
|
||||||
|
|
|
||||||
188
network/network_home_protocol.gd
Normal file
188
network/network_home_protocol.gd
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
class_name NetworkHomeProtocol
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
|
||||||
|
|
||||||
|
const CAPABILITY: StringName = &"portable_home_v1"
|
||||||
|
const RELIABLE_CHANNEL: int = NetworkProtocol.HOME_RELIABLE_CHANNEL
|
||||||
|
const MAX_ID_LENGTH: int = 96
|
||||||
|
const MAX_MESSAGE_LENGTH: int = 160
|
||||||
|
const MAX_TRANSITION_COORDINATE: float = 10000.0
|
||||||
|
const WORLD_SPACE: StringName = &"world"
|
||||||
|
|
||||||
|
enum TransitionAction {
|
||||||
|
ENTER,
|
||||||
|
EXIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func make_space_id(owner_fingerprint: String) -> StringName:
|
||||||
|
return StringName("rv:%s" % owner_fingerprint)
|
||||||
|
|
||||||
|
|
||||||
|
static func manifest_digest(home_data: Dictionary) -> String:
|
||||||
|
var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(home_data)
|
||||||
|
if sanitized.is_empty():
|
||||||
|
return ""
|
||||||
|
var canonical_owned: Array[Dictionary] = []
|
||||||
|
var owned: Dictionary = sanitized["owned_decor"]
|
||||||
|
var owned_ids: Array[String] = []
|
||||||
|
for value: Variant in owned.keys():
|
||||||
|
owned_ids.append(str(value))
|
||||||
|
owned_ids.sort()
|
||||||
|
for decor_id: String in owned_ids:
|
||||||
|
canonical_owned.append({
|
||||||
|
"id": decor_id,
|
||||||
|
"quantity": int(owned[decor_id]),
|
||||||
|
})
|
||||||
|
var placements: Array[Dictionary] = []
|
||||||
|
for value: Variant in sanitized["placements"]:
|
||||||
|
placements.append((value as Dictionary).duplicate(true))
|
||||||
|
placements.sort_custom(func(first: Dictionary, second: Dictionary) -> bool:
|
||||||
|
return str(first["instance_id"]) < str(second["instance_id"])
|
||||||
|
)
|
||||||
|
var canonical: Dictionary = {
|
||||||
|
"schema_version": int(sanitized["schema_version"]),
|
||||||
|
"tier": int(sanitized["tier"]),
|
||||||
|
"exterior_id": str(sanitized["exterior_id"]),
|
||||||
|
"privacy": int(sanitized["privacy"]),
|
||||||
|
"owned": canonical_owned,
|
||||||
|
"placements": placements,
|
||||||
|
"revision": int(sanitized["revision"]),
|
||||||
|
"next_instance_sequence": int(sanitized["next_instance_sequence"]),
|
||||||
|
}
|
||||||
|
return JSON.stringify(canonical).sha256_text()
|
||||||
|
|
||||||
|
|
||||||
|
static func manifest_signature_fields(data: Dictionary) -> Array:
|
||||||
|
return [
|
||||||
|
str(data.get("session_id", "")),
|
||||||
|
str(data.get("request_id", "")),
|
||||||
|
str(data.get("owner_fingerprint", "")),
|
||||||
|
int((data.get("home", {}) as Dictionary).get("revision", -1)),
|
||||||
|
str(data.get("digest", "")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_manifest_request(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var data: Dictionary = value
|
||||||
|
if (
|
||||||
|
not _valid_id(data.get("request_id"))
|
||||||
|
or not _valid_id(data.get("session_id"))
|
||||||
|
or not NetworkIdentityCrypto.valid_fingerprint(
|
||||||
|
data.get("owner_fingerprint")
|
||||||
|
)
|
||||||
|
or typeof(data.get("home")) != TYPE_DICTIONARY
|
||||||
|
or typeof(data.get("digest")) != TYPE_STRING
|
||||||
|
or str(data["digest"]).length() != 64
|
||||||
|
or typeof(data.get("sender_signature")) != TYPE_PACKED_BYTE_ARRAY
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(
|
||||||
|
data["home"]
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
not sanitized.is_empty()
|
||||||
|
and manifest_digest(sanitized) == str(data["digest"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_manifest_broadcast(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var data: Dictionary = value
|
||||||
|
return (
|
||||||
|
typeof(data.get("owner_peer_id")) == TYPE_INT
|
||||||
|
and int(data["owner_peer_id"]) > 0
|
||||||
|
and NetworkIdentityCrypto.valid_fingerprint(
|
||||||
|
data.get("owner_fingerprint")
|
||||||
|
)
|
||||||
|
and typeof(data.get("home")) == TYPE_DICTIONARY
|
||||||
|
and not PlayerHomeStateType.sanitize_save_data(data["home"]).is_empty()
|
||||||
|
and _valid_transform(data.get("exterior_transform"))
|
||||||
|
and typeof(data.get("interior_index")) == TYPE_INT
|
||||||
|
and int(data["interior_index"]) >= 0
|
||||||
|
and int(data["interior_index"]) < 128
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func transition_signature_fields(data: Dictionary) -> Array:
|
||||||
|
return [
|
||||||
|
str(data.get("session_id", "")),
|
||||||
|
str(data.get("request_id", "")),
|
||||||
|
int(data.get("action", -1)),
|
||||||
|
str(data.get("owner_fingerprint", "")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_transition_request(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var data: Dictionary = value
|
||||||
|
return (
|
||||||
|
_valid_id(data.get("request_id"))
|
||||||
|
and _valid_id(data.get("session_id"))
|
||||||
|
and typeof(data.get("action")) == TYPE_INT
|
||||||
|
and int(data["action"]) >= TransitionAction.ENTER
|
||||||
|
and int(data["action"]) <= TransitionAction.EXIT
|
||||||
|
and NetworkIdentityCrypto.valid_fingerprint(
|
||||||
|
data.get("owner_fingerprint")
|
||||||
|
)
|
||||||
|
and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func transform_to_array(value: Transform3D) -> Array[float]:
|
||||||
|
return [
|
||||||
|
value.basis.x.x, value.basis.x.y, value.basis.x.z,
|
||||||
|
value.basis.y.x, value.basis.y.y, value.basis.y.z,
|
||||||
|
value.basis.z.x, value.basis.z.y, value.basis.z.z,
|
||||||
|
value.origin.x, value.origin.y, value.origin.z,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
static func array_to_transform(value: Variant) -> Transform3D:
|
||||||
|
if not validate_transition_transform(value):
|
||||||
|
return Transform3D()
|
||||||
|
var fields: Array = value
|
||||||
|
return Transform3D(
|
||||||
|
Basis(
|
||||||
|
Vector3(float(fields[0]), float(fields[1]), float(fields[2])),
|
||||||
|
Vector3(float(fields[3]), float(fields[4]), float(fields[5])),
|
||||||
|
Vector3(float(fields[6]), float(fields[7]), float(fields[8])),
|
||||||
|
),
|
||||||
|
Vector3(float(fields[9]), float(fields[10]), float(fields[11])),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_transition_transform(value: Variant) -> bool:
|
||||||
|
if not _valid_transform(value):
|
||||||
|
return false
|
||||||
|
var fields: Array = value
|
||||||
|
return (
|
||||||
|
absf(float(fields[9])) <= MAX_TRANSITION_COORDINATE
|
||||||
|
and absf(float(fields[10])) <= MAX_TRANSITION_COORDINATE
|
||||||
|
and absf(float(fields[11])) <= MAX_TRANSITION_COORDINATE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _valid_transform(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_ARRAY or (value as Array).size() != 12:
|
||||||
|
return false
|
||||||
|
for component: Variant in value:
|
||||||
|
if (
|
||||||
|
typeof(component) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
or not is_finite(float(component))
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
static func _valid_id(value: Variant) -> bool:
|
||||||
|
return (
|
||||||
|
typeof(value) == TYPE_STRING
|
||||||
|
and not str(value).is_empty()
|
||||||
|
and str(value).length() <= MAX_ID_LENGTH
|
||||||
|
)
|
||||||
1
network/network_home_protocol.gd.uid
Normal file
1
network/network_home_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://fl3qwiluejhm
|
||||||
1390
network/network_home_service.gd
Normal file
1390
network/network_home_service.gd
Normal file
File diff suppressed because it is too large
Load diff
1
network/network_home_service.gd.uid
Normal file
1
network/network_home_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ong8kug36ku7
|
||||||
|
|
@ -5,6 +5,7 @@ const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
||||||
|
|
||||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||||
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
||||||
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||||
|
|
||||||
const MAX_LEDGER_ENTRIES: int = 64
|
const MAX_LEDGER_ENTRIES: int = 64
|
||||||
|
|
||||||
|
|
@ -394,6 +395,10 @@ func _apply_equipped(data: Dictionary) -> void:
|
||||||
item_id == FishingShopStockType.STANDARD_SHOVEL_ID
|
item_id == FishingShopStockType.STANDARD_SHOVEL_ID
|
||||||
and bool(data["owns_item"]),
|
and bool(data["owns_item"]),
|
||||||
)
|
)
|
||||||
|
avatar.set_active_decor_package(
|
||||||
|
DecorCatalogType.has_product(item_id)
|
||||||
|
and bool(data["owns_item"]),
|
||||||
|
)
|
||||||
equipped_state_changed.emit(
|
equipped_state_changed.emit(
|
||||||
peer_id, StringName(str(data["item_id"])), int(data["category"])
|
peer_id, StringName(str(data["item_id"])), int(data["category"])
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ extends RefCounted
|
||||||
|
|
||||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||||
|
|
||||||
const PROTOCOL_VERSION: int = 11
|
const PROTOCOL_VERSION: int = 12
|
||||||
const GAME_BUILD: String = "prealpha"
|
const GAME_BUILD: String = "prealpha"
|
||||||
const MAX_GAME_VERSION_LENGTH: int = 64
|
const MAX_GAME_VERSION_LENGTH: int = 64
|
||||||
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
||||||
|
|
@ -17,9 +17,9 @@ const MAX_SIGNATURE_LENGTH: int = 2048
|
||||||
# 8 reliable ordered session chat, 9 reliable private session mail,
|
# 8 reliable ordered session chat, 9 reliable private session mail,
|
||||||
# 10 observer-only fishing summaries, 11 reliable movement animation state,
|
# 10 observer-only fishing summaries, 11 reliable movement animation state,
|
||||||
# 12 reliable fish showcase state, 13 reliable world-spawn events,
|
# 12 reliable fish showcase state, 13 reliable world-spawn events,
|
||||||
# 14 reliable artwork state, 15 world-spawn snapshots, and 16 reliable
|
# 14 reliable artwork state, 15 world-spawn snapshots, 16 reliable fishing
|
||||||
# fishing lifecycle. Bulk world/art payloads must not head-of-line block held
|
# lifecycle, and 17 reliable portable-home state. Bulk world/art payloads must
|
||||||
# items, fishing, or animation presentation.
|
# not head-of-line block held items, fishing, animation, or home transitions.
|
||||||
const SALE_RELIABLE_CHANNEL: int = 5
|
const SALE_RELIABLE_CHANNEL: int = 5
|
||||||
const SHOP_RELIABLE_CHANNEL: int = 6
|
const SHOP_RELIABLE_CHANNEL: int = 6
|
||||||
const ITEM_RELIABLE_CHANNEL: int = 7
|
const ITEM_RELIABLE_CHANNEL: int = 7
|
||||||
|
|
@ -30,7 +30,8 @@ const SHOWCASE_RELIABLE_CHANNEL: int = 12
|
||||||
const WORLD_SPAWN_RELIABLE_CHANNEL: int = 13
|
const WORLD_SPAWN_RELIABLE_CHANNEL: int = 13
|
||||||
const DRAWING_RELIABLE_CHANNEL: int = 14
|
const DRAWING_RELIABLE_CHANNEL: int = 14
|
||||||
const FISHING_RELIABLE_CHANNEL: int = 16
|
const FISHING_RELIABLE_CHANNEL: int = 16
|
||||||
const ENET_CHANNEL_COUNT: int = 17
|
const HOME_RELIABLE_CHANNEL: int = 17
|
||||||
|
const ENET_CHANNEL_COUNT: int = 18
|
||||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
||||||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||||
const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1"
|
const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1"
|
||||||
|
|
@ -45,6 +46,7 @@ const WORLD_LAYOUT_CAPABILITY: String = "world_layout_v1"
|
||||||
const FRIENDS_CAPABILITY: String = "friends_v1"
|
const FRIENDS_CAPABILITY: String = "friends_v1"
|
||||||
const MOVEMENT_RECONCILIATION_CAPABILITY: String = "movement_reconciliation_v2"
|
const MOVEMENT_RECONCILIATION_CAPABILITY: String = "movement_reconciliation_v2"
|
||||||
const FISHING_REPLICATION_CAPABILITY: String = "fishing_replication_v2"
|
const FISHING_REPLICATION_CAPABILITY: String = "fishing_replication_v2"
|
||||||
|
const HOME_CAPABILITY: String = "portable_home_v1"
|
||||||
const DEFAULT_WORLD_SEED: int = 13001
|
const DEFAULT_WORLD_SEED: int = 13001
|
||||||
const MAX_WORLD_SEED: int = 2147483646
|
const MAX_WORLD_SEED: int = 2147483646
|
||||||
|
|
||||||
|
|
@ -202,6 +204,7 @@ static func make_client_hello(
|
||||||
FRIENDS_CAPABILITY,
|
FRIENDS_CAPABILITY,
|
||||||
MOVEMENT_RECONCILIATION_CAPABILITY,
|
MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||||
FISHING_REPLICATION_CAPABILITY,
|
FISHING_REPLICATION_CAPABILITY,
|
||||||
|
HOME_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
"cosmetic_snapshot": cosmetic_snapshot,
|
"cosmetic_snapshot": cosmetic_snapshot,
|
||||||
"identity_fingerprint": identity_fingerprint,
|
"identity_fingerprint": identity_fingerprint,
|
||||||
|
|
@ -342,6 +345,7 @@ static func make_server_hello(
|
||||||
FRIENDS_CAPABILITY,
|
FRIENDS_CAPABILITY,
|
||||||
MOVEMENT_RECONCILIATION_CAPABILITY,
|
MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||||
FISHING_REPLICATION_CAPABILITY,
|
FISHING_REPLICATION_CAPABILITY,
|
||||||
|
HOME_CAPABILITY,
|
||||||
"chat_v1",
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const MAX_PENDING_MOVEMENT_INPUTS: int = 96
|
||||||
const ANIMATION_REFRESH_INTERVAL: float = 1.0
|
const ANIMATION_REFRESH_INTERVAL: float = 1.0
|
||||||
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
||||||
const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
|
const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
|
||||||
|
const SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE: float = 12.0
|
||||||
|
|
||||||
const MOVEMENT_FLAG_JUMP: int = 1 << 0
|
const MOVEMENT_FLAG_JUMP: int = 1 << 0
|
||||||
const MOVEMENT_FLAG_SPRINT: int = 1 << 1
|
const MOVEMENT_FLAG_SPRINT: int = 1 << 1
|
||||||
|
|
@ -124,6 +125,9 @@ var _pending_movement_inputs: Array[Dictionary] = []
|
||||||
var _snapshot_accumulator: float = 0.0
|
var _snapshot_accumulator: float = 0.0
|
||||||
var _movement_snapshot_tick: int = 0
|
var _movement_snapshot_tick: int = 0
|
||||||
var _last_local_snapshot_received_msec: int = 0
|
var _last_local_snapshot_received_msec: int = 0
|
||||||
|
var _local_space_transition_guard_active: bool = false
|
||||||
|
var _local_space_transition_guard_position: Vector3 = Vector3.ZERO
|
||||||
|
var _local_space_transition_minimum_ack: int = 0
|
||||||
var _animation_refresh_accumulator: float = 0.0
|
var _animation_refresh_accumulator: float = 0.0
|
||||||
var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {}
|
var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {}
|
||||||
var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {}
|
var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {}
|
||||||
|
|
@ -141,6 +145,7 @@ var _last_server_protocol_version: int = 0
|
||||||
var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
|
var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
|
||||||
var _last_server_world_layout: StringName = WorldLayout.GENERATED
|
var _last_server_world_layout: StringName = WorldLayout.GENERATED
|
||||||
var _server_capabilities: PackedStringArray = PackedStringArray()
|
var _server_capabilities: PackedStringArray = PackedStringArray()
|
||||||
|
var _peer_space_ids: Dictionary[int, StringName] = {}
|
||||||
var _profile_ready: bool = false
|
var _profile_ready: bool = false
|
||||||
var _player_identity: PlayerIdentityStore
|
var _player_identity: PlayerIdentityStore
|
||||||
var _host_identity: HostIdentityStore
|
var _host_identity: HostIdentityStore
|
||||||
|
|
@ -334,6 +339,7 @@ func _register_player_host() -> void:
|
||||||
NetworkProtocol.FRIENDS_CAPABILITY,
|
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||||
|
NetworkProtocol.HOME_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||||
|
|
@ -661,6 +667,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
||||||
NetworkProtocol.FRIENDS_CAPABILITY,
|
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||||
|
NetworkProtocol.HOME_CAPABILITY,
|
||||||
"chat_v1",
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
|
|
@ -685,6 +692,55 @@ func get_authenticated_peer_ids() -> Array[int]:
|
||||||
return _registry.get_peer_ids()
|
return _registry.get_peer_ids()
|
||||||
|
|
||||||
|
|
||||||
|
func set_peer_space(peer_id: int, space_id: StringName) -> bool:
|
||||||
|
if not _registry.has_peer(peer_id) or space_id.is_empty():
|
||||||
|
return false
|
||||||
|
_peer_space_ids[peer_id] = space_id
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_peer_space(peer_id: int) -> StringName:
|
||||||
|
return _peer_space_ids.get(peer_id, &"world")
|
||||||
|
|
||||||
|
|
||||||
|
func peers_share_space(first_peer_id: int, second_peer_id: int) -> bool:
|
||||||
|
return get_peer_space(first_peer_id) == get_peer_space(second_peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func apply_authoritative_space_transition(
|
||||||
|
peer_id: int,
|
||||||
|
target_transform: Transform3D,
|
||||||
|
acknowledged_input: int,
|
||||||
|
) -> bool:
|
||||||
|
if not target_transform.origin.is_finite():
|
||||||
|
return false
|
||||||
|
var local_peer_id: int = get_local_peer_id()
|
||||||
|
var local_transition: bool = peer_id == local_peer_id
|
||||||
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
|
# Remote home transitions can arrive before their player-spawn message. Keep
|
||||||
|
# the new space assignment so the later spawn inherits correct visibility;
|
||||||
|
# only a missing local avatar is a transition failure.
|
||||||
|
if avatar == null:
|
||||||
|
return not local_transition
|
||||||
|
avatar.apply_network_space_transition(target_transform, local_transition)
|
||||||
|
if (
|
||||||
|
local_transition
|
||||||
|
and state == State.JOINED_CLIENT
|
||||||
|
):
|
||||||
|
# Movement snapshots use another ENet channel and a pre-transition audit
|
||||||
|
# can therefore arrive after the reliable home transition. Clear local
|
||||||
|
# prediction and reject those old-world snapshots until the host publishes
|
||||||
|
# its first audit from the new space.
|
||||||
|
_pending_movement_inputs.clear()
|
||||||
|
_last_local_snapshot_received_msec = 0
|
||||||
|
_last_input_state_hash = 0
|
||||||
|
_idle_input_accumulator = IDLE_INPUT_INTERVAL
|
||||||
|
_local_space_transition_guard_active = true
|
||||||
|
_local_space_transition_guard_position = target_transform.origin
|
||||||
|
_local_space_transition_minimum_ack = maxi(acknowledged_input, 0)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func get_peer_rtt_ms(peer_id: int) -> int:
|
func get_peer_rtt_ms(peer_id: int) -> int:
|
||||||
if peer_id == 1:
|
if peer_id == 1:
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1030,6 +1086,7 @@ func _on_peer_disconnected(peer_id: int) -> void:
|
||||||
_pending_identity_challenges.erase(peer_id)
|
_pending_identity_challenges.erase(peer_id)
|
||||||
_authenticated_identity_cache.erase(peer_id)
|
_authenticated_identity_cache.erase(peer_id)
|
||||||
_operator_peer_ids.erase(peer_id)
|
_operator_peer_ids.erase(peer_id)
|
||||||
|
_peer_space_ids.erase(peer_id)
|
||||||
_last_animation_state_by_peer.erase(peer_id)
|
_last_animation_state_by_peer.erase(peer_id)
|
||||||
_pending_animation_state_by_peer.erase(peer_id)
|
_pending_animation_state_by_peer.erase(peer_id)
|
||||||
var recovery_attempt: String = _recovery_attempts.get(peer_id, "")
|
var recovery_attempt: String = _recovery_attempts.get(peer_id, "")
|
||||||
|
|
@ -1313,6 +1370,7 @@ func submit_client_hello(data: Dictionary) -> void:
|
||||||
for required_capability: String in [
|
for required_capability: String in [
|
||||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||||
|
NetworkProtocol.HOME_CAPABILITY,
|
||||||
]:
|
]:
|
||||||
if required_capability not in client_capabilities:
|
if required_capability not in client_capabilities:
|
||||||
_reject_peer(
|
_reject_peer(
|
||||||
|
|
@ -1546,6 +1604,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
||||||
for required_capability: String in [
|
for required_capability: String in [
|
||||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||||
|
NetworkProtocol.HOME_CAPABILITY,
|
||||||
]:
|
]:
|
||||||
if required_capability not in _server_capabilities:
|
if required_capability not in _server_capabilities:
|
||||||
_teardown_peer()
|
_teardown_peer()
|
||||||
|
|
@ -1578,6 +1637,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
||||||
NetworkProtocol.FRIENDS_CAPABILITY,
|
NetworkProtocol.FRIENDS_CAPABILITY,
|
||||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||||
|
NetworkProtocol.HOME_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||||
|
|
@ -1623,6 +1683,7 @@ func receive_peer_despawn(peer_id: int) -> void:
|
||||||
if state != State.JOINED_CLIENT:
|
if state != State.JOINED_CLIENT:
|
||||||
return
|
return
|
||||||
_operator_peer_ids.erase(peer_id)
|
_operator_peer_ids.erase(peer_id)
|
||||||
|
_peer_space_ids.erase(peer_id)
|
||||||
_last_animation_state_by_peer.erase(peer_id)
|
_last_animation_state_by_peer.erase(peer_id)
|
||||||
_pending_animation_state_by_peer.erase(peer_id)
|
_pending_animation_state_by_peer.erase(peer_id)
|
||||||
_registry.remove_peer(peer_id)
|
_registry.remove_peer(peer_id)
|
||||||
|
|
@ -2247,6 +2308,8 @@ func _broadcast_movement_snapshots() -> void:
|
||||||
continue
|
continue
|
||||||
var snapshots: Array = []
|
var snapshots: Array = []
|
||||||
for subject_id: int in peer_ids:
|
for subject_id: int in peer_ids:
|
||||||
|
if not peers_share_space(recipient_id, subject_id):
|
||||||
|
continue
|
||||||
var subject_avatar: Player = _spawn_service.get_avatar(subject_id)
|
var subject_avatar: Player = _spawn_service.get_avatar(subject_id)
|
||||||
if subject_avatar == null:
|
if subject_avatar == null:
|
||||||
continue
|
continue
|
||||||
|
|
@ -2315,7 +2378,7 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
|
||||||
_animation_refresh_accumulator,
|
_animation_refresh_accumulator,
|
||||||
ANIMATION_REFRESH_INTERVAL,
|
ANIMATION_REFRESH_INTERVAL,
|
||||||
)
|
)
|
||||||
var updates: Array = []
|
var updates_by_subject: Dictionary[int, Array] = {}
|
||||||
for subject_id: int in peer_ids:
|
for subject_id: int in peer_ids:
|
||||||
var avatar: Player = _spawn_service.get_avatar(subject_id)
|
var avatar: Player = _spawn_service.get_avatar(subject_id)
|
||||||
if avatar == null:
|
if avatar == null:
|
||||||
|
|
@ -2333,12 +2396,18 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
|
||||||
_last_animation_state_by_peer[subject_id] = state.duplicate(true)
|
_last_animation_state_by_peer[subject_id] = state.duplicate(true)
|
||||||
var encoded: Array = _encode_movement_animation(subject_id, state)
|
var encoded: Array = _encode_movement_animation(subject_id, state)
|
||||||
if not encoded.is_empty():
|
if not encoded.is_empty():
|
||||||
updates.append(encoded)
|
updates_by_subject[subject_id] = encoded
|
||||||
if updates.is_empty():
|
if updates_by_subject.is_empty():
|
||||||
return
|
return
|
||||||
for recipient_id: int in peer_ids:
|
for recipient_id: int in peer_ids:
|
||||||
if recipient_id == 1:
|
if recipient_id == 1:
|
||||||
continue
|
continue
|
||||||
|
var updates: Array = []
|
||||||
|
for subject_id: int in updates_by_subject:
|
||||||
|
if peers_share_space(recipient_id, subject_id):
|
||||||
|
updates.append(updates_by_subject[subject_id])
|
||||||
|
if updates.is_empty():
|
||||||
|
continue
|
||||||
receive_movement_animations.rpc_id(recipient_id, updates)
|
receive_movement_animations.rpc_id(recipient_id, updates)
|
||||||
_movement_animation_packets_sent += 1
|
_movement_animation_packets_sent += 1
|
||||||
_movement_animation_states_sent += updates.size()
|
_movement_animation_states_sent += updates.size()
|
||||||
|
|
@ -2538,6 +2607,8 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
||||||
if avatar == null:
|
if avatar == null:
|
||||||
continue
|
continue
|
||||||
if peer_id == local_peer_id:
|
if peer_id == local_peer_id:
|
||||||
|
if _reject_stale_space_transition_snapshot(snapshot):
|
||||||
|
continue
|
||||||
_discard_acknowledged_movement_inputs(
|
_discard_acknowledged_movement_inputs(
|
||||||
int(snapshot.get("acknowledged_input", 0))
|
int(snapshot.get("acknowledged_input", 0))
|
||||||
)
|
)
|
||||||
|
|
@ -2555,6 +2626,33 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _reject_stale_space_transition_snapshot(snapshot: Dictionary) -> bool:
|
||||||
|
if not _local_space_transition_guard_active:
|
||||||
|
return false
|
||||||
|
var position_values: Array = snapshot.get("position", [])
|
||||||
|
if position_values.size() != 3:
|
||||||
|
return true
|
||||||
|
var position := Vector3(
|
||||||
|
float(position_values[0]),
|
||||||
|
float(position_values[1]),
|
||||||
|
float(position_values[2]),
|
||||||
|
)
|
||||||
|
var acknowledged_input: int = int(
|
||||||
|
snapshot.get("acknowledged_input", 0)
|
||||||
|
)
|
||||||
|
if acknowledged_input < _local_space_transition_minimum_ack:
|
||||||
|
return true
|
||||||
|
if acknowledged_input > _local_space_transition_minimum_ack:
|
||||||
|
_local_space_transition_guard_active = false
|
||||||
|
return false
|
||||||
|
if position.distance_to(_local_space_transition_guard_position) > (
|
||||||
|
SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
_local_space_transition_guard_active = false
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void:
|
func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void:
|
||||||
while (
|
while (
|
||||||
not _pending_movement_inputs.is_empty()
|
not _pending_movement_inputs.is_empty()
|
||||||
|
|
@ -2883,11 +2981,15 @@ func _teardown_peer() -> void:
|
||||||
_snapshot_accumulator = 0.0
|
_snapshot_accumulator = 0.0
|
||||||
_movement_snapshot_tick = 0
|
_movement_snapshot_tick = 0
|
||||||
_last_local_snapshot_received_msec = 0
|
_last_local_snapshot_received_msec = 0
|
||||||
|
_local_space_transition_guard_active = false
|
||||||
|
_local_space_transition_guard_position = Vector3.ZERO
|
||||||
|
_local_space_transition_minimum_ack = 0
|
||||||
_animation_refresh_accumulator = 0.0
|
_animation_refresh_accumulator = 0.0
|
||||||
_last_animation_state_by_peer.clear()
|
_last_animation_state_by_peer.clear()
|
||||||
_pending_animation_state_by_peer.clear()
|
_pending_animation_state_by_peer.clear()
|
||||||
_last_local_animation_action_signature.clear()
|
_last_local_animation_action_signature.clear()
|
||||||
_server_capabilities = PackedStringArray()
|
_server_capabilities = PackedStringArray()
|
||||||
|
_peer_space_ids.clear()
|
||||||
_server_identity_fingerprint = ""
|
_server_identity_fingerprint = ""
|
||||||
_server_identity_public_key = ""
|
_server_identity_public_key = ""
|
||||||
_session_identity_keys.clear()
|
_session_identity_keys.clear()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ extends RefCounted
|
||||||
const CAPABILITY: StringName = &"shop_v1"
|
const CAPABILITY: StringName = &"shop_v1"
|
||||||
const ART_CAPABILITY: StringName = &"art_shop_v1"
|
const ART_CAPABILITY: StringName = &"art_shop_v1"
|
||||||
const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1"
|
const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1"
|
||||||
|
const DECOR_CAPABILITY: StringName = &"portable_home_v1"
|
||||||
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
|
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
|
||||||
const MAX_ID_LENGTH: int = 96
|
const MAX_ID_LENGTH: int = 96
|
||||||
const MAX_MESSAGE_LENGTH: int = 160
|
const MAX_MESSAGE_LENGTH: int = 160
|
||||||
|
|
@ -18,6 +19,7 @@ enum ProductCategory {
|
||||||
ART_KIT,
|
ART_KIT,
|
||||||
ART_UPGRADE,
|
ART_UPGRADE,
|
||||||
BACKPACK_CAPACITY_UPGRADE,
|
BACKPACK_CAPACITY_UPGRADE,
|
||||||
|
DECOR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@ const ItemDataType = preload("res://items/item_data.gd")
|
||||||
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
||||||
const OwnedItemType = preload("res://items/owned_item.gd")
|
const OwnedItemType = preload("res://items/owned_item.gd")
|
||||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||||
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||||
|
|
||||||
const SHOP_ID: StringName = &"main_fishing_shop"
|
const FISHING_SHOP_ID: StringName = &"main_fishing_shop"
|
||||||
|
const DECOR_SHOP_ID: StringName = &"rv_decor_shop"
|
||||||
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
|
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
|
||||||
const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
|
const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
|
||||||
const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
|
const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
|
||||||
|
|
@ -31,6 +33,7 @@ var _session: NetworkSession
|
||||||
var _spawn_service: PlayerSpawnService
|
var _spawn_service: PlayerSpawnService
|
||||||
var _network_fishing: NetworkFishingService
|
var _network_fishing: NetworkFishingService
|
||||||
var _interaction: FishingShopInteraction
|
var _interaction: FishingShopInteraction
|
||||||
|
var _decor_interaction: DecorShopInteraction
|
||||||
var _wallet: PlayerWallet
|
var _wallet: PlayerWallet
|
||||||
var _bag: PlayerBag
|
var _bag: PlayerBag
|
||||||
var _item_catalog: ItemCatalog
|
var _item_catalog: ItemCatalog
|
||||||
|
|
@ -47,6 +50,8 @@ var _received_results: Dictionary[String, bool] = {}
|
||||||
var _applied_results: Dictionary[String, bool] = {}
|
var _applied_results: Dictionary[String, bool] = {}
|
||||||
var _pending_local_request: Dictionary = {}
|
var _pending_local_request: Dictionary = {}
|
||||||
var _reservations: PlayerAssetReservationService
|
var _reservations: PlayerAssetReservationService
|
||||||
|
var _home_state: PlayerHomeState
|
||||||
|
var _network_home: NetworkHomeService
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
|
|
@ -63,6 +68,9 @@ func setup(
|
||||||
save_manager: PlayerSaveManager,
|
save_manager: PlayerSaveManager,
|
||||||
reservations: PlayerAssetReservationService,
|
reservations: PlayerAssetReservationService,
|
||||||
inventory_layout: PlayerInventoryLayout = null,
|
inventory_layout: PlayerInventoryLayout = null,
|
||||||
|
home_state: PlayerHomeState = null,
|
||||||
|
network_home: NetworkHomeService = null,
|
||||||
|
decor_interaction: DecorShopInteraction = null,
|
||||||
) -> void:
|
) -> void:
|
||||||
_session = session
|
_session = session
|
||||||
_spawn_service = spawn_service
|
_spawn_service = spawn_service
|
||||||
|
|
@ -77,6 +85,9 @@ func setup(
|
||||||
_save_manager = save_manager
|
_save_manager = save_manager
|
||||||
_reservations = reservations
|
_reservations = reservations
|
||||||
_inventory_layout = inventory_layout
|
_inventory_layout = inventory_layout
|
||||||
|
_home_state = home_state
|
||||||
|
_network_home = network_home
|
||||||
|
_decor_interaction = decor_interaction
|
||||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||||
|
|
@ -87,6 +98,12 @@ func set_shop_interaction(interaction: FishingShopInteraction) -> void:
|
||||||
_interaction = interaction
|
_interaction = interaction
|
||||||
|
|
||||||
|
|
||||||
|
func set_decor_shop_interaction(
|
||||||
|
interaction: DecorShopInteraction,
|
||||||
|
) -> void:
|
||||||
|
_decor_interaction = interaction
|
||||||
|
|
||||||
|
|
||||||
func can_request_purchase() -> bool:
|
func can_request_purchase() -> bool:
|
||||||
return (
|
return (
|
||||||
_session != null
|
_session != null
|
||||||
|
|
@ -124,6 +141,19 @@ func can_request_backpack_purchase() -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func can_request_decor_purchase() -> bool:
|
||||||
|
return (
|
||||||
|
can_request_purchase()
|
||||||
|
and _home_state != null
|
||||||
|
and (
|
||||||
|
_session.is_host()
|
||||||
|
or _session.supports_server_capability(
|
||||||
|
NetworkShopProtocol.DECOR_CAPABILITY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func is_local_purchase_pending() -> bool:
|
func is_local_purchase_pending() -> bool:
|
||||||
return not _pending_local_request.is_empty()
|
return not _pending_local_request.is_empty()
|
||||||
|
|
||||||
|
|
@ -193,6 +223,15 @@ func request_backpack_capacity_upgrade() -> String:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func request_decor(product_id: StringName) -> String:
|
||||||
|
return _request_purchase(
|
||||||
|
product_id,
|
||||||
|
NetworkShopProtocol.ProductCategory.DECOR,
|
||||||
|
1,
|
||||||
|
_home_state.get_owned_count(product_id) if _home_state != null else 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func request_art_kit() -> String:
|
func request_art_kit() -> String:
|
||||||
return _request_purchase(
|
return _request_purchase(
|
||||||
ArtShopStockType.ART_KIT_ITEM_ID,
|
ArtShopStockType.ART_KIT_ITEM_ID,
|
||||||
|
|
@ -269,11 +308,25 @@ func _request_purchase(
|
||||||
product_id, category, 0, 0,
|
product_id, category, 0, 0,
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
|
if (
|
||||||
|
category == NetworkShopProtocol.ProductCategory.DECOR
|
||||||
|
and not can_request_decor_purchase()
|
||||||
|
):
|
||||||
|
local_purchase_finished.emit(
|
||||||
|
"", false, "Decor require a newer server.",
|
||||||
|
product_id, category, 0, 0,
|
||||||
|
)
|
||||||
|
return ""
|
||||||
var request_id: String = _new_id("shop")
|
var request_id: String = _new_id("shop")
|
||||||
|
var shop_id: StringName = (
|
||||||
|
DECOR_SHOP_ID
|
||||||
|
if category == NetworkShopProtocol.ProductCategory.DECOR
|
||||||
|
else FISHING_SHOP_ID
|
||||||
|
)
|
||||||
var request: Dictionary = {
|
var request: Dictionary = {
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
"session_id": _session.get_session_id(),
|
"session_id": _session.get_session_id(),
|
||||||
"shop_id": str(SHOP_ID),
|
"shop_id": str(shop_id),
|
||||||
"product_id": str(product_id),
|
"product_id": str(product_id),
|
||||||
"category": category,
|
"category": category,
|
||||||
"quantity": quantity,
|
"quantity": quantity,
|
||||||
|
|
@ -345,16 +398,34 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void:
|
||||||
_rejected_result(data, "A purchase is already pending.")
|
_rejected_result(data, "A purchase is already pending.")
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if not _is_shop_available_for_peer(peer_id):
|
var requested_shop_id := StringName(str(data["shop_id"]))
|
||||||
|
if not _is_shop_available_for_peer(peer_id, requested_shop_id):
|
||||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
|
var requested_interaction: FishingShopInteraction = (
|
||||||
|
_decor_interaction
|
||||||
|
if requested_shop_id == DECOR_SHOP_ID
|
||||||
|
else _interaction
|
||||||
|
)
|
||||||
var message: String = (
|
var message: String = (
|
||||||
"The shop is unavailable."
|
"The shop is unavailable."
|
||||||
if avatar == null or _interaction == null
|
if avatar == null or requested_interaction == null
|
||||||
else "Move closer to the shop."
|
else "Move closer to the shop."
|
||||||
)
|
)
|
||||||
_record_and_send(peer_id, _rejected_result(data, message))
|
_record_and_send(peer_id, _rejected_result(data, message))
|
||||||
return
|
return
|
||||||
var result: Dictionary = _build_authoritative_result(peer_id, data)
|
var result: Dictionary = _build_authoritative_result(peer_id, data)
|
||||||
|
if (
|
||||||
|
bool(result.get("accepted", false))
|
||||||
|
and int(result.get("category", -1))
|
||||||
|
== NetworkShopProtocol.ProductCategory.DECOR
|
||||||
|
and _network_home != null
|
||||||
|
):
|
||||||
|
_network_home.authorize_decor_purchase(
|
||||||
|
peer_id,
|
||||||
|
StringName(str(result["product_id"])),
|
||||||
|
int(result["resulting_state"]),
|
||||||
|
str(result["result_id"]),
|
||||||
|
)
|
||||||
_pending_by_peer[peer_id] = request_id
|
_pending_by_peer[peer_id] = request_id
|
||||||
_record_and_send(peer_id, result)
|
_record_and_send(peer_id, result)
|
||||||
|
|
||||||
|
|
@ -368,10 +439,24 @@ func _build_authoritative_result(
|
||||||
var quantity: int = request["quantity"]
|
var quantity: int = request["quantity"]
|
||||||
var wallet_balance: int = request["wallet_balance"]
|
var wallet_balance: int = request["wallet_balance"]
|
||||||
var current_state: int = request["current_state"]
|
var current_state: int = request["current_state"]
|
||||||
|
if (
|
||||||
|
category == NetworkShopProtocol.ProductCategory.DECOR
|
||||||
|
and _network_home != null
|
||||||
|
):
|
||||||
|
var authoritative_count: int = (
|
||||||
|
_network_home.get_authoritative_owned_count(peer_id, product_id)
|
||||||
|
)
|
||||||
|
if authoritative_count >= 0:
|
||||||
|
current_state = authoritative_count
|
||||||
var cost: int = -1
|
var cost: int = -1
|
||||||
var resulting_state: int = current_state
|
var resulting_state: int = current_state
|
||||||
var rejection: String = ""
|
var rejection: String = ""
|
||||||
if StringName(str(request["shop_id"])) != SHOP_ID:
|
var expected_shop_id: StringName = (
|
||||||
|
DECOR_SHOP_ID
|
||||||
|
if category == NetworkShopProtocol.ProductCategory.DECOR
|
||||||
|
else FISHING_SHOP_ID
|
||||||
|
)
|
||||||
|
if StringName(str(request["shop_id"])) != expected_shop_id:
|
||||||
rejection = "The shop is unavailable."
|
rejection = "The shop is unavailable."
|
||||||
elif quantity < 1:
|
elif quantity < 1:
|
||||||
rejection = "Purchase could not be completed."
|
rejection = "Purchase could not be completed."
|
||||||
|
|
@ -545,6 +630,19 @@ func _build_authoritative_result(
|
||||||
resulting_state = PlayerArtUnlocks.resulting_mask(
|
resulting_state = PlayerArtUnlocks.resulting_mask(
|
||||||
current_state, product_id
|
current_state, product_id
|
||||||
)
|
)
|
||||||
|
NetworkShopProtocol.ProductCategory.DECOR:
|
||||||
|
cost = DecorCatalogType.get_price(product_id)
|
||||||
|
if (
|
||||||
|
not DecorCatalogType.has_product(product_id)
|
||||||
|
or quantity != 1
|
||||||
|
or current_state < 0
|
||||||
|
or current_state
|
||||||
|
>= DecorCatalogType.MAX_OWNED_PER_PRODUCT
|
||||||
|
or cost < 0
|
||||||
|
):
|
||||||
|
rejection = "Decor is unavailable."
|
||||||
|
else:
|
||||||
|
resulting_state = current_state + 1
|
||||||
if rejection.is_empty() and (cost < 0 or wallet_balance < cost):
|
if rejection.is_empty() and (cost < 0 or wallet_balance < cost):
|
||||||
rejection = "Insufficient funds."
|
rejection = "Insufficient funds."
|
||||||
if not rejection.is_empty():
|
if not rejection.is_empty():
|
||||||
|
|
@ -667,6 +765,9 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
||||||
if _inventory_layout != null else 0
|
if _inventory_layout != null else 0
|
||||||
)
|
)
|
||||||
var art_snapshot: int = _art_unlocks.get_unlock_mask()
|
var art_snapshot: int = _art_unlocks.get_unlock_mask()
|
||||||
|
var home_snapshot: Dictionary = (
|
||||||
|
_home_state.to_save_data() if _home_state != null else {}
|
||||||
|
)
|
||||||
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)
|
||||||
|
|
@ -676,6 +777,8 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
||||||
if _inventory_layout != null:
|
if _inventory_layout != null:
|
||||||
_inventory_layout.restore_backpack_level(backpack_snapshot)
|
_inventory_layout.restore_backpack_level(backpack_snapshot)
|
||||||
_art_unlocks.restore_mask(art_snapshot)
|
_art_unlocks.restore_mask(art_snapshot)
|
||||||
|
if _home_state != null and not home_snapshot.is_empty():
|
||||||
|
_home_state.restore_from_save_data(home_snapshot)
|
||||||
_wallet.restore_balance(wallet_snapshot)
|
_wallet.restore_balance(wallet_snapshot)
|
||||||
_save_manager.save_if_dirty()
|
_save_manager.save_if_dirty()
|
||||||
_fail_local_apply(data, "Purchase could not be completed.")
|
_fail_local_apply(data, "Purchase could not be completed.")
|
||||||
|
|
@ -696,6 +799,7 @@ func _validate_local_result(data: Dictionary) -> String:
|
||||||
or _cooler_capacity == null
|
or _cooler_capacity == null
|
||||||
or _art_unlocks == null
|
or _art_unlocks == null
|
||||||
or _save_manager == null
|
or _save_manager == null
|
||||||
|
or _home_state == null
|
||||||
or _wallet.get_balance() != int(data["expected_wallet"])
|
or _wallet.get_balance() != int(data["expected_wallet"])
|
||||||
or str(data["product_id"])
|
or str(data["product_id"])
|
||||||
!= str(_pending_local_request.get("product_id", ""))
|
!= str(_pending_local_request.get("product_id", ""))
|
||||||
|
|
@ -806,6 +910,20 @@ func _validate_local_result(data: Dictionary) -> String:
|
||||||
or cost != ArtShopStockType.UPGRADE_PRICE
|
or cost != ArtShopStockType.UPGRADE_PRICE
|
||||||
):
|
):
|
||||||
return "Purchase could not be completed."
|
return "Purchase could not be completed."
|
||||||
|
NetworkShopProtocol.ProductCategory.DECOR:
|
||||||
|
if (
|
||||||
|
not DecorCatalogType.has_product(product_id)
|
||||||
|
or _home_state.get_owned_count(product_id) != expected_state
|
||||||
|
or resulting_state != expected_state + 1
|
||||||
|
or cost != DecorCatalogType.get_price(product_id)
|
||||||
|
or expected_state >= DecorCatalogType.MAX_OWNED_PER_PRODUCT
|
||||||
|
or not _bag.can_add_item(product_id, 1)
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
"Your inventory is full."
|
||||||
|
if not _bag.can_add_item(product_id, 1)
|
||||||
|
else "Purchase could not be completed."
|
||||||
|
)
|
||||||
_:
|
_:
|
||||||
return "Purchase could not be completed."
|
return "Purchase could not be completed."
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -845,6 +963,12 @@ func _apply_local_product(data: Dictionary) -> bool:
|
||||||
return _art_unlocks.purchase_product(
|
return _art_unlocks.purchase_product(
|
||||||
product_id, _wallet, int(data["total_cost"])
|
product_id, _wallet, int(data["total_cost"])
|
||||||
)
|
)
|
||||||
|
NetworkShopProtocol.ProductCategory.DECOR:
|
||||||
|
return (
|
||||||
|
_wallet.debit(int(data["total_cost"]))
|
||||||
|
and _bag.add_item(product_id, 1)
|
||||||
|
and _home_state.add_owned_decor(product_id, 1)
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -921,7 +1045,7 @@ func _handle_acknowledgement(
|
||||||
peer_id: int,
|
peer_id: int,
|
||||||
result_id: String,
|
result_id: String,
|
||||||
request_id: String,
|
request_id: String,
|
||||||
_applied: bool,
|
applied: bool,
|
||||||
message: String,
|
message: String,
|
||||||
) -> void:
|
) -> void:
|
||||||
if (
|
if (
|
||||||
|
|
@ -933,20 +1057,32 @@ func _handle_acknowledgement(
|
||||||
or _result_owners.get(result_id, 0) != peer_id
|
or _result_owners.get(result_id, 0) != peer_id
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
if _network_home != null:
|
||||||
|
_network_home.finalize_decor_purchase(
|
||||||
|
peer_id, result_id, applied
|
||||||
|
)
|
||||||
_acknowledged_results[result_id] = true
|
_acknowledged_results[result_id] = true
|
||||||
_bound_dictionary(_acknowledged_results)
|
_bound_dictionary(_acknowledged_results)
|
||||||
if _pending_by_peer.get(peer_id, "") == request_id:
|
if _pending_by_peer.get(peer_id, "") == request_id:
|
||||||
_pending_by_peer.erase(peer_id)
|
_pending_by_peer.erase(peer_id)
|
||||||
|
|
||||||
|
|
||||||
func _is_shop_available_for_peer(peer_id: int) -> bool:
|
func _is_shop_available_for_peer(
|
||||||
|
peer_id: int,
|
||||||
|
shop_id: StringName,
|
||||||
|
) -> bool:
|
||||||
|
var interaction: FishingShopInteraction = (
|
||||||
|
_decor_interaction
|
||||||
|
if shop_id == DECOR_SHOP_ID
|
||||||
|
else _interaction
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
_session == null
|
_session == null
|
||||||
or not _session.is_host()
|
or not _session.is_host()
|
||||||
or not _session.is_gameplay_session_active()
|
or not _session.is_gameplay_session_active()
|
||||||
or _spawn_service == null
|
or _spawn_service == null
|
||||||
or _interaction == null
|
or interaction == null
|
||||||
or not is_instance_valid(_interaction)
|
or not is_instance_valid(interaction)
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
|
|
@ -957,7 +1093,7 @@ func _is_shop_available_for_peer(peer_id: int) -> bool:
|
||||||
_network_fishing == null
|
_network_fishing == null
|
||||||
or not _network_fishing.has_peer_attempt(peer_id)
|
or not _network_fishing.has_peer_attempt(peer_id)
|
||||||
)
|
)
|
||||||
and _interaction.is_avatar_in_range(avatar)
|
and interaction.is_avatar_in_range(avatar)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,11 @@ func activate(
|
||||||
_pointer_screen_position = _clamped_pointer_position(
|
_pointer_screen_position = _clamped_pointer_position(
|
||||||
initial_pointer_position
|
initial_pointer_position
|
||||||
)
|
)
|
||||||
_prior_mouse_mode = Input.mouse_mode
|
# Art-kit activation can occur while the prior right-click camera drag is
|
||||||
|
# still held. Stop that drag before recording a return mode, then restore
|
||||||
|
# normal camera availability so right-click continues to work while drawing.
|
||||||
|
_stop_camera_drag_for_art_kit()
|
||||||
|
_prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode)
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
_update_aim()
|
_update_aim()
|
||||||
_refresh_stencil_visibility()
|
_refresh_stencil_visibility()
|
||||||
|
|
@ -266,6 +270,10 @@ func deactivate() -> void:
|
||||||
_eraser_mode = false
|
_eraser_mode = false
|
||||||
_clear_armed_guide_action(false)
|
_clear_armed_guide_action(false)
|
||||||
_camera_look_active = false
|
_camera_look_active = false
|
||||||
|
# Do not leave a drag captured when drawing is closed by a menu, hotbar
|
||||||
|
# change, or session transition. The next camera drag must start on a fresh
|
||||||
|
# physical right-click press.
|
||||||
|
_stop_camera_drag_for_art_kit()
|
||||||
_reset_stroke()
|
_reset_stroke()
|
||||||
_selected_canvas_id = ""
|
_selected_canvas_id = ""
|
||||||
_hovered_canvas_id = ""
|
_hovered_canvas_id = ""
|
||||||
|
|
@ -273,10 +281,24 @@ func deactivate() -> void:
|
||||||
_aim_hit.clear()
|
_aim_hit.clear()
|
||||||
_hide_previews()
|
_hide_previews()
|
||||||
_refresh_stencil_visibility()
|
_refresh_stencil_visibility()
|
||||||
Input.mouse_mode = _prior_mouse_mode
|
Input.mouse_mode = _get_restorable_mouse_mode(_prior_mouse_mode)
|
||||||
_emit_hud_state("")
|
_emit_hud_state("")
|
||||||
|
|
||||||
|
|
||||||
|
func _stop_camera_drag_for_art_kit() -> void:
|
||||||
|
if _local_player == null or not is_instance_valid(_local_player):
|
||||||
|
return
|
||||||
|
var camera_input_enabled: bool = _local_player.is_camera_input_enabled()
|
||||||
|
_local_player.set_camera_input_enabled(false)
|
||||||
|
_local_player.set_camera_input_enabled(camera_input_enabled)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
|
||||||
|
if mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
|
return Input.MOUSE_MODE_VISIBLE
|
||||||
|
return mouse_mode
|
||||||
|
|
||||||
|
|
||||||
func is_active() -> bool:
|
func is_active() -> bool:
|
||||||
return _active
|
return _active
|
||||||
|
|
||||||
|
|
|
||||||
166
player/player.gd
166
player/player.gd
|
|
@ -427,6 +427,9 @@ class ShowcaseCameraSnapshot:
|
||||||
@export_range(0.0, 1.0, 0.01) var controller_camera_deadzone: float = 0.2
|
@export_range(0.0, 1.0, 0.01) var controller_camera_deadzone: float = 0.2
|
||||||
@export var invert_camera_y: bool = false
|
@export var invert_camera_y: bool = false
|
||||||
var _swap_hotbar_camera_scroll: bool = false
|
var _swap_hotbar_camera_scroll: bool = false
|
||||||
|
var _rv_interior_camera_mode: bool = false
|
||||||
|
var _rv_interior_environment: Environment
|
||||||
|
var _rv_previous_camera_environment: Environment
|
||||||
@export_range(-89.0, 0.0, 0.5) var minimum_pitch_degrees: float = -65.0
|
@export_range(-89.0, 0.0, 0.5) var minimum_pitch_degrees: float = -65.0
|
||||||
@export_range(0.0, 89.0, 0.5) var maximum_pitch_degrees: float = 45.0
|
@export_range(0.0, 89.0, 0.5) var maximum_pitch_degrees: float = 45.0
|
||||||
@export var minimum_zoom: float = 2.0
|
@export var minimum_zoom: float = 2.0
|
||||||
|
|
@ -471,6 +474,8 @@ var _held_fish_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
|
||||||
var _held_fish_sprite: Sprite3D
|
var _held_fish_sprite: Sprite3D
|
||||||
var _held_art_kit_display: Node3D
|
var _held_art_kit_display: Node3D
|
||||||
var _held_art_kit_sprite: Sprite3D
|
var _held_art_kit_sprite: Sprite3D
|
||||||
|
var _held_decor_package_display: Node3D
|
||||||
|
var _held_decor_package_visible: bool = false
|
||||||
var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
|
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"))
|
||||||
|
|
@ -992,6 +997,7 @@ func _initialize_held_item_attachment() -> void:
|
||||||
_held_art_kit_sprite = attachment.get_node(
|
_held_art_kit_sprite = attachment.get_node(
|
||||||
"HeldArtKitDisplay/HeldArtKitSprite"
|
"HeldArtKitDisplay/HeldArtKitSprite"
|
||||||
) as Sprite3D
|
) as Sprite3D
|
||||||
|
_held_decor_package_display = _build_decor_package_display(attachment)
|
||||||
_catch_display = attachment.get_node("CatchDisplay") as Node3D
|
_catch_display = attachment.get_node("CatchDisplay") as Node3D
|
||||||
_catch_attachment_offset = _catch_display.position
|
_catch_attachment_offset = _catch_display.position
|
||||||
_catch_sprite = attachment.get_node("CatchDisplay/CatchSprite") as Sprite3D
|
_catch_sprite = attachment.get_node("CatchDisplay/CatchSprite") as Sprite3D
|
||||||
|
|
@ -1322,9 +1328,7 @@ func _update_character_animation() -> void:
|
||||||
and supports_network_animation_action(animation_action_id)
|
and supports_network_animation_action(animation_action_id)
|
||||||
and _character_animation_player.has_animation(animation_action_id)
|
and _character_animation_player.has_animation(animation_action_id)
|
||||||
)
|
)
|
||||||
var held_show_item_visible: bool = (
|
var held_show_item_visible: bool = _has_held_show_item()
|
||||||
_held_fish_visible or _held_art_kit_visible
|
|
||||||
)
|
|
||||||
var requested_animation: Array[StringName] = []
|
var requested_animation: Array[StringName] = []
|
||||||
if _pocket_visual_active:
|
if _pocket_visual_active:
|
||||||
requested_animation = [_pocket_visual_animation]
|
requested_animation = [_pocket_visual_animation]
|
||||||
|
|
@ -1766,7 +1770,11 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
|
||||||
var mouse_zoom_in: bool = (
|
var mouse_zoom_in: bool = (
|
||||||
event is InputEventMouseButton
|
event is InputEventMouseButton
|
||||||
and event.shift_pressed != _swap_hotbar_camera_scroll
|
and (
|
||||||
|
(not event.shift_pressed)
|
||||||
|
if _rv_interior_camera_mode
|
||||||
|
else event.shift_pressed != _swap_hotbar_camera_scroll
|
||||||
|
)
|
||||||
and event.is_action_pressed("camera_zoom_in")
|
and event.is_action_pressed("camera_zoom_in")
|
||||||
)
|
)
|
||||||
var controller_zoom_in: bool = (
|
var controller_zoom_in: bool = (
|
||||||
|
|
@ -1775,7 +1783,11 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
)
|
)
|
||||||
var mouse_zoom_out: bool = (
|
var mouse_zoom_out: bool = (
|
||||||
event is InputEventMouseButton
|
event is InputEventMouseButton
|
||||||
and event.shift_pressed != _swap_hotbar_camera_scroll
|
and (
|
||||||
|
(not event.shift_pressed)
|
||||||
|
if _rv_interior_camera_mode
|
||||||
|
else event.shift_pressed != _swap_hotbar_camera_scroll
|
||||||
|
)
|
||||||
and event.is_action_pressed("camera_zoom_out")
|
and event.is_action_pressed("camera_zoom_out")
|
||||||
)
|
)
|
||||||
var controller_zoom_out: bool = (
|
var controller_zoom_out: bool = (
|
||||||
|
|
@ -1963,7 +1975,11 @@ func get_active_gameplay_camera() -> Camera3D:
|
||||||
|
|
||||||
|
|
||||||
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,
|
||||||
|
1.25 if _rv_interior_camera_mode else minimum_zoom,
|
||||||
|
16.0 if _rv_interior_camera_mode else maximum_zoom,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _apply_axis_deadzone(value: float) -> float:
|
func _apply_axis_deadzone(value: float) -> float:
|
||||||
|
|
@ -2077,6 +2093,37 @@ func reset_network_movement_state() -> void:
|
||||||
_local_prediction_largest_error = 0.0
|
_local_prediction_largest_error = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func apply_network_space_transition(
|
||||||
|
target_transform: Transform3D,
|
||||||
|
align_local_camera: bool,
|
||||||
|
) -> void:
|
||||||
|
if not target_transform.origin.is_finite():
|
||||||
|
return
|
||||||
|
# A room transition is a discontinuity, not ordinary movement. Preserve the
|
||||||
|
# host's input acknowledgement while clearing every motion, animation, and
|
||||||
|
# reconciliation state that could pull the avatar toward the previous space.
|
||||||
|
var acknowledged_input: int = _last_network_input_sequence
|
||||||
|
reset_network_movement_state()
|
||||||
|
_last_network_input_sequence = acknowledged_input
|
||||||
|
_apply_network_casting(false)
|
||||||
|
velocity = Vector3.ZERO
|
||||||
|
global_transform = Transform3D(Basis.IDENTITY, target_transform.origin)
|
||||||
|
var target_yaw: float = target_transform.basis.get_euler().y
|
||||||
|
_visuals.rotation.y = target_yaw
|
||||||
|
_network_camera_yaw = target_yaw
|
||||||
|
_network_target_position = global_position
|
||||||
|
_network_target_velocity = Vector3.ZERO
|
||||||
|
_network_target_visual_yaw = target_yaw
|
||||||
|
_network_snapshot_age = 0.0
|
||||||
|
_network_snapshot_ready = true
|
||||||
|
_authoritative_movement_history.clear()
|
||||||
|
if align_local_camera:
|
||||||
|
_set_camera_dragging(false)
|
||||||
|
if _free_camera_active:
|
||||||
|
_set_free_camera_active(false)
|
||||||
|
_camera_yaw.rotation.y = target_yaw
|
||||||
|
|
||||||
|
|
||||||
func capture_network_input(sequence: int) -> Dictionary:
|
func capture_network_input(sequence: int) -> Dictionary:
|
||||||
if _sitting_intent_pending and _sitting_intent_sequence < 0:
|
if _sitting_intent_pending and _sitting_intent_sequence < 0:
|
||||||
_sitting_intent_sequence = sequence
|
_sitting_intent_sequence = sequence
|
||||||
|
|
@ -3021,6 +3068,49 @@ func apply_camera_settings(
|
||||||
_swap_hotbar_camera_scroll = swap_hotbar_camera_scroll
|
_swap_hotbar_camera_scroll = swap_hotbar_camera_scroll
|
||||||
|
|
||||||
|
|
||||||
|
func set_rv_interior_camera_mode(
|
||||||
|
enabled: bool,
|
||||||
|
excluded_wall_rids: Array[RID] = [],
|
||||||
|
) -> void:
|
||||||
|
if enabled != _rv_interior_camera_mode:
|
||||||
|
if enabled:
|
||||||
|
_rv_previous_camera_environment = _camera.environment
|
||||||
|
_camera.environment = _get_rv_interior_environment()
|
||||||
|
if _free_camera != null:
|
||||||
|
_free_camera.environment = _camera.environment
|
||||||
|
else:
|
||||||
|
_camera.environment = _rv_previous_camera_environment
|
||||||
|
if _free_camera != null:
|
||||||
|
_free_camera.environment = _camera.environment
|
||||||
|
_rv_previous_camera_environment = null
|
||||||
|
_rv_interior_camera_mode = enabled
|
||||||
|
if _spring_arm == null:
|
||||||
|
return
|
||||||
|
_spring_arm.clear_excluded_objects()
|
||||||
|
if enabled:
|
||||||
|
for collider_rid: RID in excluded_wall_rids:
|
||||||
|
if collider_rid.is_valid():
|
||||||
|
_spring_arm.add_excluded_object(collider_rid)
|
||||||
|
_set_target_zoom(_target_zoom)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_rv_interior_environment() -> Environment:
|
||||||
|
if _rv_interior_environment != null:
|
||||||
|
return _rv_interior_environment
|
||||||
|
_rv_interior_environment = Environment.new()
|
||||||
|
_rv_interior_environment.background_mode = Environment.BG_COLOR
|
||||||
|
_rv_interior_environment.background_color = Color("020506")
|
||||||
|
_rv_interior_environment.ambient_light_source = (
|
||||||
|
Environment.AMBIENT_SOURCE_COLOR
|
||||||
|
)
|
||||||
|
_rv_interior_environment.ambient_light_color = Color("d6caaa")
|
||||||
|
_rv_interior_environment.ambient_light_energy = 1.0
|
||||||
|
_rv_interior_environment.reflected_light_source = (
|
||||||
|
Environment.REFLECTION_SOURCE_DISABLED
|
||||||
|
)
|
||||||
|
return _rv_interior_environment
|
||||||
|
|
||||||
|
|
||||||
func is_camera_input_enabled() -> bool:
|
func is_camera_input_enabled() -> bool:
|
||||||
return _camera_input_enabled
|
return _camera_input_enabled
|
||||||
|
|
||||||
|
|
@ -3147,6 +3237,23 @@ func set_active_shovel(should_show: bool) -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func set_active_decor_package(should_show: bool) -> void:
|
||||||
|
var changed: bool = _held_decor_package_visible != should_show
|
||||||
|
_held_decor_package_visible = should_show
|
||||||
|
if _held_decor_package_display != null:
|
||||||
|
_held_decor_package_display.visible = should_show
|
||||||
|
if should_show:
|
||||||
|
_fishing_rod.visible = false
|
||||||
|
if _catching_net != null:
|
||||||
|
_catching_net.visible = false
|
||||||
|
if _shovel != null:
|
||||||
|
_shovel.visible = false
|
||||||
|
elif changed:
|
||||||
|
_apply_active_rod_visibility()
|
||||||
|
if changed:
|
||||||
|
_update_character_animation()
|
||||||
|
|
||||||
|
|
||||||
func _apply_fishing_rod_model(rod: FishingRodDataType) -> void:
|
func _apply_fishing_rod_model(rod: FishingRodDataType) -> void:
|
||||||
var next_id: StringName = rod.item_id if rod != null else StringName()
|
var next_id: StringName = rod.item_id if rod != null else StringName()
|
||||||
if next_id == _active_fishing_rod_id:
|
if next_id == _active_fishing_rod_id:
|
||||||
|
|
@ -3319,7 +3426,52 @@ func set_held_fish(
|
||||||
|
|
||||||
|
|
||||||
func _has_held_show_item() -> bool:
|
func _has_held_show_item() -> bool:
|
||||||
return _held_fish_visible or _held_art_kit_visible
|
return (
|
||||||
|
_held_fish_visible
|
||||||
|
or _held_art_kit_visible
|
||||||
|
or _held_decor_package_visible
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_decor_package_display(parent: Node3D) -> Node3D:
|
||||||
|
var display := Node3D.new()
|
||||||
|
display.name = "HeldDecorPackageDisplay"
|
||||||
|
display.position = Vector3(0.0, 0.1, 0.04)
|
||||||
|
display.rotation = Vector3(0.15, 0.0, -0.55)
|
||||||
|
display.visible = false
|
||||||
|
parent.add_child(display)
|
||||||
|
|
||||||
|
var sack := MeshInstance3D.new()
|
||||||
|
sack.name = "Sack"
|
||||||
|
var sack_mesh := SphereMesh.new()
|
||||||
|
sack_mesh.radius = 0.22
|
||||||
|
sack_mesh.height = 0.48
|
||||||
|
sack_mesh.radial_segments = 8
|
||||||
|
sack_mesh.rings = 4
|
||||||
|
sack_mesh.material = _decor_package_material(Color("a87843"))
|
||||||
|
sack.mesh = sack_mesh
|
||||||
|
display.add_child(sack)
|
||||||
|
|
||||||
|
var tie := MeshInstance3D.new()
|
||||||
|
tie.name = "Tie"
|
||||||
|
var tie_mesh := CylinderMesh.new()
|
||||||
|
tie_mesh.top_radius = 0.1
|
||||||
|
tie_mesh.bottom_radius = 0.12
|
||||||
|
tie_mesh.height = 0.08
|
||||||
|
tie_mesh.radial_segments = 8
|
||||||
|
tie_mesh.material = _decor_package_material(Color("513829"))
|
||||||
|
tie.mesh = tie_mesh
|
||||||
|
tie.position = Vector3(0.0, 0.22, 0.0)
|
||||||
|
display.add_child(tie)
|
||||||
|
return display
|
||||||
|
|
||||||
|
|
||||||
|
func _decor_package_material(color: Color) -> StandardMaterial3D:
|
||||||
|
var material := StandardMaterial3D.new()
|
||||||
|
material.albedo_color = color
|
||||||
|
material.roughness = 1.0
|
||||||
|
material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX
|
||||||
|
return material
|
||||||
|
|
||||||
|
|
||||||
func _clear_held_fish() -> void:
|
func _clear_held_fish() -> void:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ config_version=5
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="straywild"
|
config/name="straywild"
|
||||||
config/version="0.19.0-alpha"
|
config/version="0.20.2-alpha"
|
||||||
run/main_scene="res://main/main.tscn"
|
run/main_scene="res://main/main.tscn"
|
||||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||||
config/icon="res://art/exported/system_icons/straywild_256.png"
|
config/icon="res://art/exported/system_icons/straywild_256.png"
|
||||||
|
|
@ -260,7 +260,7 @@ alternate_fishing={
|
||||||
|
|
||||||
[network]
|
[network]
|
||||||
|
|
||||||
discovery/base_url="https://discovery.straywild.io"
|
discovery/base_url="https://discovery.straywild.io/patron-test"
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,9 @@ const WorldWeatherServiceType = preload(
|
||||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||||
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
|
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
|
||||||
const PlayerType = preload("res://player/player.gd")
|
const PlayerType = preload("res://player/player.gd")
|
||||||
|
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
|
||||||
|
|
||||||
const SAVE_VERSION: int = 10
|
const SAVE_VERSION: int = 11
|
||||||
const LEGACY_SAVE_FILENAME := "player_save.json"
|
const LEGACY_SAVE_FILENAME := "player_save.json"
|
||||||
const ARCHIVE_EXTENSION := ".nfsave"
|
const ARCHIVE_EXTENSION := ".nfsave"
|
||||||
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
|
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
|
||||||
|
|
@ -76,6 +77,7 @@ class LoadSnapshot:
|
||||||
WorldWeatherServiceType.SUNNY_DURATION_RANGE.x
|
WorldWeatherServiceType.SUNNY_DURATION_RANGE.x
|
||||||
)
|
)
|
||||||
var jobs_data: Dictionary = PlayerJobServiceType.default_save_data()
|
var jobs_data: Dictionary = PlayerJobServiceType.default_save_data()
|
||||||
|
var home_data: Dictionary = PlayerHomeStateType.default_save_data()
|
||||||
|
|
||||||
|
|
||||||
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
|
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
|
||||||
|
|
@ -96,6 +98,7 @@ var _world_time: WorldTimeServiceType
|
||||||
var _world_weather: WorldWeatherServiceType
|
var _world_weather: WorldWeatherServiceType
|
||||||
var _jobs: PlayerJobServiceType
|
var _jobs: PlayerJobServiceType
|
||||||
var _player: PlayerType
|
var _player: PlayerType
|
||||||
|
var _home_state: PlayerHomeStateType
|
||||||
var _autosave_timer: Timer
|
var _autosave_timer: Timer
|
||||||
var _is_configured: bool = false
|
var _is_configured: bool = false
|
||||||
var _is_restoring: bool = false
|
var _is_restoring: bool = false
|
||||||
|
|
@ -175,6 +178,7 @@ func setup(
|
||||||
world_weather: WorldWeatherServiceType,
|
world_weather: WorldWeatherServiceType,
|
||||||
jobs: PlayerJobServiceType,
|
jobs: PlayerJobServiceType,
|
||||||
player: PlayerType,
|
player: PlayerType,
|
||||||
|
home_state: PlayerHomeStateType,
|
||||||
) -> void:
|
) -> void:
|
||||||
_inventory = inventory
|
_inventory = inventory
|
||||||
_collection_log = collection_log
|
_collection_log = collection_log
|
||||||
|
|
@ -192,6 +196,7 @@ func setup(
|
||||||
_world_weather = world_weather
|
_world_weather = world_weather
|
||||||
_jobs = jobs
|
_jobs = jobs
|
||||||
_player = player
|
_player = player
|
||||||
|
_home_state = home_state
|
||||||
_is_configured = (
|
_is_configured = (
|
||||||
_inventory != null
|
_inventory != null
|
||||||
and _collection_log != null
|
and _collection_log != null
|
||||||
|
|
@ -209,6 +214,7 @@ func setup(
|
||||||
and _world_weather != null
|
and _world_weather != null
|
||||||
and _jobs != null
|
and _jobs != null
|
||||||
and _player != null
|
and _player != null
|
||||||
|
and _home_state != null
|
||||||
)
|
)
|
||||||
if not _is_configured:
|
if not _is_configured:
|
||||||
push_error("PlayerSaveManager setup is missing required references.")
|
push_error("PlayerSaveManager setup is missing required references.")
|
||||||
|
|
@ -255,6 +261,8 @@ func setup(
|
||||||
_player.active_bait_changed.connect(_on_active_tackle_changed)
|
_player.active_bait_changed.connect(_on_active_tackle_changed)
|
||||||
if not _player.active_lure_changed.is_connected(_on_active_tackle_changed):
|
if not _player.active_lure_changed.is_connected(_on_active_tackle_changed):
|
||||||
_player.active_lure_changed.connect(_on_active_tackle_changed)
|
_player.active_lure_changed.connect(_on_active_tackle_changed)
|
||||||
|
if not _home_state.changed.is_connected(_mark_dirty):
|
||||||
|
_home_state.changed.connect(_mark_dirty)
|
||||||
|
|
||||||
|
|
||||||
func load_player_data() -> bool:
|
func load_player_data() -> bool:
|
||||||
|
|
@ -360,6 +368,9 @@ func load_player_data() -> bool:
|
||||||
snapshot.world_weather_seconds_remaining,
|
snapshot.world_weather_seconds_remaining,
|
||||||
)
|
)
|
||||||
var jobs_restored: bool = _jobs.restore_from_save_data(snapshot.jobs_data)
|
var jobs_restored: bool = _jobs.restore_from_save_data(snapshot.jobs_data)
|
||||||
|
var home_restored: bool = _home_state.restore_from_save_data(
|
||||||
|
snapshot.home_data
|
||||||
|
)
|
||||||
_is_restoring = false
|
_is_restoring = false
|
||||||
if (
|
if (
|
||||||
not inventory_restored
|
not inventory_restored
|
||||||
|
|
@ -376,13 +387,14 @@ func load_player_data() -> bool:
|
||||||
or not world_time_restored
|
or not world_time_restored
|
||||||
or not world_weather_restored
|
or not world_weather_restored
|
||||||
or not jobs_restored
|
or not jobs_restored
|
||||||
|
or not home_restored
|
||||||
):
|
):
|
||||||
push_error(
|
push_error(
|
||||||
(
|
(
|
||||||
"Validated player save could not be restored: "
|
"Validated player save could not be restored: "
|
||||||
+ "inventory=%s collection=%s wallet=%s bag=%s tackle=%s hotbar=%s "
|
+ "inventory=%s collection=%s wallet=%s bag=%s tackle=%s hotbar=%s "
|
||||||
+ "upgrades=%s cooler=%s layout=%s art=%s experience=%s "
|
+ "upgrades=%s cooler=%s layout=%s art=%s experience=%s "
|
||||||
+ "time=%s weather=%s jobs=%s"
|
+ "time=%s weather=%s jobs=%s home=%s"
|
||||||
)
|
)
|
||||||
% [
|
% [
|
||||||
inventory_restored,
|
inventory_restored,
|
||||||
|
|
@ -399,6 +411,7 @@ func load_player_data() -> bool:
|
||||||
world_time_restored,
|
world_time_restored,
|
||||||
world_weather_restored,
|
world_weather_restored,
|
||||||
jobs_restored,
|
jobs_restored,
|
||||||
|
home_restored,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
|
|
@ -899,6 +912,7 @@ func _build_save_dictionary() -> Dictionary:
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
"jobs": _jobs.to_save_data(),
|
"jobs": _jobs.to_save_data(),
|
||||||
|
"home": _home_state.to_save_data(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -913,6 +927,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
or typeof(save_data.get("inventory_layout")) != TYPE_DICTIONARY
|
or typeof(save_data.get("inventory_layout")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("experience")) != TYPE_DICTIONARY
|
or typeof(save_data.get("experience")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("jobs")) != TYPE_DICTIONARY
|
or typeof(save_data.get("jobs")) != TYPE_DICTIONARY
|
||||||
|
or typeof(save_data.get("home")) != TYPE_DICTIONARY
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
var wallet_data: Dictionary = save_data["wallet"]
|
var wallet_data: Dictionary = save_data["wallet"]
|
||||||
|
|
@ -924,6 +939,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
var inventory_layout_data: Dictionary = save_data["inventory_layout"]
|
var inventory_layout_data: Dictionary = save_data["inventory_layout"]
|
||||||
var experience_data: Dictionary = save_data["experience"]
|
var experience_data: Dictionary = save_data["experience"]
|
||||||
var jobs_data: Dictionary = save_data["jobs"]
|
var jobs_data: Dictionary = save_data["jobs"]
|
||||||
|
var home_data: Dictionary = save_data["home"]
|
||||||
var world_data: Dictionary = {}
|
var world_data: Dictionary = {}
|
||||||
if typeof(save_data.get("world")) == TYPE_DICTIONARY:
|
if typeof(save_data.get("world")) == TYPE_DICTIONARY:
|
||||||
world_data = save_data["world"]
|
world_data = save_data["world"]
|
||||||
|
|
@ -1006,6 +1022,12 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
if not PlayerJobServiceType.validate_save_data(jobs_data):
|
if not PlayerJobServiceType.validate_save_data(jobs_data):
|
||||||
return null
|
return null
|
||||||
snapshot.jobs_data = jobs_data.duplicate(true)
|
snapshot.jobs_data = jobs_data.duplicate(true)
|
||||||
|
var sanitized_home: Dictionary = PlayerHomeStateType.sanitize_save_data(
|
||||||
|
home_data
|
||||||
|
)
|
||||||
|
if sanitized_home.is_empty():
|
||||||
|
return null
|
||||||
|
snapshot.home_data = sanitized_home
|
||||||
if world_data.has("time_hours"):
|
if world_data.has("time_hours"):
|
||||||
snapshot.world_time_hours = _read_world_time_hours(
|
snapshot.world_time_hours = _read_world_time_hours(
|
||||||
world_data["time_hours"]
|
world_data["time_hours"]
|
||||||
|
|
@ -1340,6 +1362,8 @@ func _migrate_save(
|
||||||
migrated = _migrate_version_8_to_9(migrated)
|
migrated = _migrate_version_8_to_9(migrated)
|
||||||
9:
|
9:
|
||||||
migrated = _migrate_version_9_to_10(migrated)
|
migrated = _migrate_version_9_to_10(migrated)
|
||||||
|
10:
|
||||||
|
migrated = _migrate_version_10_to_11(migrated)
|
||||||
_:
|
_:
|
||||||
return {}
|
return {}
|
||||||
if migrated.is_empty():
|
if migrated.is_empty():
|
||||||
|
|
@ -1597,6 +1621,13 @@ func _migrate_version_9_to_10(data: Dictionary) -> Dictionary:
|
||||||
return migrated
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
|
func _migrate_version_10_to_11(data: Dictionary) -> Dictionary:
|
||||||
|
var migrated: Dictionary = data.duplicate(true)
|
||||||
|
migrated["home"] = PlayerHomeStateType.default_save_data()
|
||||||
|
migrated["save_version"] = 11
|
||||||
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
func _mark_dirty() -> void:
|
func _mark_dirty() -> void:
|
||||||
if (
|
if (
|
||||||
_is_restoring
|
_is_restoring
|
||||||
|
|
@ -1866,6 +1897,7 @@ func _restore_defaults() -> void:
|
||||||
_world_layout = WorldLayoutType.GENERATED
|
_world_layout = WorldLayoutType.GENERATED
|
||||||
_world_seed = DEFAULT_WORLD_SEED
|
_world_seed = DEFAULT_WORLD_SEED
|
||||||
_jobs.reset_to_defaults()
|
_jobs.reset_to_defaults()
|
||||||
|
_home_state.reset_to_defaults()
|
||||||
_is_restoring = false
|
_is_restoring = false
|
||||||
_is_dirty = false
|
_is_dirty = false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ 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 BUILD_ROOT="${PROJECT_ROOT}/builds/v0.19.0-alpha"
|
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.20.2-alpha"
|
||||||
readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
|
readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
|
||||||
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
|
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
|
||||||
readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
|
readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
|
||||||
readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
|
readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
|
||||||
readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/netfishing"
|
readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/straywild"
|
||||||
readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.19.0-alpha-windows-x86_64.zip"
|
readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.20.2-alpha-windows-x86_64.zip"
|
||||||
readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.19.0-alpha-linux-x86_64.zip"
|
readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.20.2-alpha-linux-x86_64.zip"
|
||||||
readonly GODOT_BIN="${GODOT_BIN:-godot}"
|
readonly GODOT_BIN="${GODOT_BIN:-godot}"
|
||||||
|
|
||||||
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then
|
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then
|
||||||
|
|
@ -77,6 +77,9 @@ straywild code is licensed under GPL-3.0-or-later.
|
||||||
Source repository: ${SOURCE_URL}
|
Source repository: ${SOURCE_URL}
|
||||||
Source revision at build time: ${SOURCE_COMMIT}
|
Source revision at build time: ${SOURCE_COMMIT}
|
||||||
|
|
||||||
|
This private testing release is distributed with an exact corresponding
|
||||||
|
source archive named straywild-v0.20.2-alpha-source.tar.gz.
|
||||||
|
|
||||||
If this package includes uncommitted development changes, it must not be
|
If this package includes uncommitted development changes, it must not be
|
||||||
publicly redistributed until the exact corresponding source is also made
|
publicly redistributed until the exact corresponding source is also made
|
||||||
available.
|
available.
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ cat >"${GAME_ROOT}/licenses/SOURCE-CODE.md" <<EOF
|
||||||
|
|
||||||
straywild code is licensed under GPL-3.0-or-later.
|
straywild code is licensed under GPL-3.0-or-later.
|
||||||
|
|
||||||
Source repository: https://forge.makearmy.io/woofmeow/netfishing
|
Source repository: https://forge.makearmy.io/woofmeow/straywild
|
||||||
Exact source revision: ${TAG_COMMIT}
|
Exact source revision: ${TAG_COMMIT}
|
||||||
Release tag: ${RELEASE_TAG}
|
Release tag: ${RELEASE_TAG}
|
||||||
PortMaster packaging revision: ${HEAD_COMMIT}
|
PortMaster packaging revision: ${HEAD_COMMIT}
|
||||||
|
|
@ -236,7 +236,7 @@ unzip -tq "${ARCHIVE}" >/dev/null
|
||||||
readonly TOP_LEVELS="$(
|
readonly TOP_LEVELS="$(
|
||||||
unzip -Z1 "${ARCHIVE}" | cut -d/ -f1 | sort -u
|
unzip -Z1 "${ARCHIVE}" | cut -d/ -f1 | sort -u
|
||||||
)"
|
)"
|
||||||
readonly EXPECTED_TOP_LEVELS="$(printf '%s\n' straywild.sh straywild)"
|
readonly EXPECTED_TOP_LEVELS="$(printf '%s\n' straywild straywild.sh)"
|
||||||
if [[ "${TOP_LEVELS}" != "${EXPECTED_TOP_LEVELS}" ]]; then
|
if [[ "${TOP_LEVELS}" != "${EXPECTED_TOP_LEVELS}" ]]; then
|
||||||
echo "Unexpected PortMaster archive roots:" >&2
|
echo "Unexpected PortMaster archive roots:" >&2
|
||||||
printf '%s\n' "${TOP_LEVELS}" >&2
|
printf '%s\n' "${TOP_LEVELS}" >&2
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ readonly -a NETWORK_TESTS=(
|
||||||
"tests/fish_showcase_multiplayer_validation.gd"
|
"tests/fish_showcase_multiplayer_validation.gd"
|
||||||
"tests/friend_multiplayer_validation.gd"
|
"tests/friend_multiplayer_validation.gd"
|
||||||
"tests/fishing_multiplayer_validation.gd"
|
"tests/fishing_multiplayer_validation.gd"
|
||||||
|
"tests/home_spawn_multiplayer_validation.gd"
|
||||||
"tests/job_multiplayer_validation.gd"
|
"tests/job_multiplayer_validation.gd"
|
||||||
"tests/movement_multiplayer_validation.gd"
|
"tests/movement_multiplayer_validation.gd"
|
||||||
"tests/operator_multiplayer_validation.gd"
|
"tests/operator_multiplayer_validation.gd"
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,15 @@ func _run() -> void:
|
||||||
for _frame: int in 8:
|
for _frame: int in 8:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||||
|
)))
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_prepare_host_world", WorldLayout.STARTER_ISLAND, 1
|
||||||
|
)))
|
||||||
assert(bool(main.call("_prepare_private_host")))
|
assert(bool(main.call("_prepare_private_host")))
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(save_manager.initialize_new_game())
|
assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
await physics_frame
|
await physics_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
@ -94,11 +100,7 @@ func _run() -> void:
|
||||||
assert(keyboard_mapping.reset_mapping())
|
assert(keyboard_mapping.reset_mapping())
|
||||||
await process_frame
|
await process_frame
|
||||||
var shop_prompt_key := game_ui.get_node("%ShopPromptKey") as Label
|
var shop_prompt_key := game_ui.get_node("%ShopPromptKey") as Label
|
||||||
var storage_prompt_message := game_ui.get_node(
|
|
||||||
"%StoragePromptMessage"
|
|
||||||
) as Label
|
|
||||||
assert(shop_prompt_key.text == "E")
|
assert(shop_prompt_key.text == "E")
|
||||||
assert(storage_prompt_message.text == "E open storage")
|
|
||||||
var remapped_interact := InputEventKey.new()
|
var remapped_interact := InputEventKey.new()
|
||||||
remapped_interact.physical_keycode = KEY_F
|
remapped_interact.physical_keycode = KEY_F
|
||||||
remapped_interact.pressed = true
|
remapped_interact.pressed = true
|
||||||
|
|
@ -108,7 +110,6 @@ func _run() -> void:
|
||||||
))
|
))
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(shop_prompt_key.text == "F")
|
assert(shop_prompt_key.text == "F")
|
||||||
assert(storage_prompt_message.text == "F open storage")
|
|
||||||
assert(keyboard_mapping.reset_mapping())
|
assert(keyboard_mapping.reset_mapping())
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(toolbar.get_parent() == chat_ui.get_parent())
|
assert(toolbar.get_parent() == chat_ui.get_parent())
|
||||||
|
|
@ -416,6 +417,12 @@ func _run() -> void:
|
||||||
)
|
)
|
||||||
assert(normal_camera.is_position_behind(chat_anchor))
|
assert(normal_camera.is_position_behind(chat_anchor))
|
||||||
assert(not free_camera.is_position_behind(chat_anchor))
|
assert(not free_camera.is_position_behind(chat_anchor))
|
||||||
|
var shop_prompt := game_ui.get_node("%ShopPrompt") as Control
|
||||||
|
var storage_prompt := game_ui.get_node("%StoragePrompt") as Control
|
||||||
|
game_ui.set_shop_prompt_visible(true, chat_anchor)
|
||||||
|
game_ui.set_storage_prompt_visible(true, chat_anchor)
|
||||||
|
assert(shop_prompt.visible)
|
||||||
|
assert(storage_prompt.visible)
|
||||||
chat_ui.show_local_speech("hello there friend")
|
chat_ui.show_local_speech("hello there friend")
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(player.get("_speech_mouth_active")))
|
assert(bool(player.get("_speech_mouth_active")))
|
||||||
|
|
|
||||||
|
|
@ -1356,6 +1356,24 @@ func _validate_player_menu_nested_controller_back() -> void:
|
||||||
== LogbookPageType.ControllerZone.ENTRIES,
|
== LogbookPageType.ControllerZone.ENTRIES,
|
||||||
"Global player-menu Back did not return Logbook to creature selection.",
|
"Global player-menu Back did not return Logbook to creature selection.",
|
||||||
)
|
)
|
||||||
|
logbook_page.call("_set_index_open", true, false)
|
||||||
|
logbook_page.set(
|
||||||
|
"_controller_zone",
|
||||||
|
LogbookPageType.ControllerZone.INDEX,
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
bool(menu.call("consume_escape")),
|
||||||
|
"Global player-menu Back did not consume the Logbook index zone.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
menu.visible and not bool(logbook_page.get("_index_open")),
|
||||||
|
"Global player-menu Back did not return from the Logbook index page.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
int(logbook_page.get("_controller_zone"))
|
||||||
|
== LogbookPageType.ControllerZone.TABS,
|
||||||
|
"Global player-menu Back did not return Logbook index focus to tabs.",
|
||||||
|
)
|
||||||
|
|
||||||
var net_page := menu.get("_the_net_page") as Control
|
var net_page := menu.get("_the_net_page") as Control
|
||||||
net_page.set("_active", true)
|
net_page.set("_active", true)
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,14 @@ func _run() -> void:
|
||||||
for _frame: int in 8:
|
for _frame: int in 8:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||||
|
)))
|
||||||
|
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
|
||||||
assert(bool(main.call("_prepare_private_host")))
|
assert(bool(main.call("_prepare_private_host")))
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(save_manager.initialize_new_game())
|
assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
for _frame: int in 8:
|
for _frame: int in 8:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
@ -42,7 +47,6 @@ func _run() -> void:
|
||||||
var catalog := main.get("fish_catalog") as FishPool
|
var catalog := main.get("fish_catalog") as FishPool
|
||||||
var sale_service := main.get_node("%NetworkSaleService") as NetworkSaleService
|
var sale_service := main.get_node("%NetworkSaleService") as NetworkSaleService
|
||||||
var shop_service := main.get_node("%NetworkShopService") as NetworkShopService
|
var shop_service := main.get_node("%NetworkShopService") as NetworkShopService
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
|
||||||
var reservations := (
|
var reservations := (
|
||||||
main.get_node("%PlayerAssetReservationService")
|
main.get_node("%PlayerAssetReservationService")
|
||||||
as PlayerAssetReservationService
|
as PlayerAssetReservationService
|
||||||
|
|
@ -554,7 +558,10 @@ func _test_host_backpack_purchase(
|
||||||
for expected_capacity: int in [18, 27, 36]:
|
for expected_capacity: int in [18, 27, 36]:
|
||||||
_shop_result.clear()
|
_shop_result.clear()
|
||||||
assert(not shop_service.request_backpack_capacity_upgrade().is_empty())
|
assert(not shop_service.request_backpack_capacity_upgrade().is_empty())
|
||||||
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
assert(
|
||||||
|
not _shop_result.is_empty() and bool(_shop_result[1]),
|
||||||
|
"Backpack purchase failed: %s" % [_shop_result],
|
||||||
|
)
|
||||||
assert(layout.get_inventory_capacity() == expected_capacity)
|
assert(layout.get_inventory_capacity() == expected_capacity)
|
||||||
assert(player.wallet.get_balance() == balance_before - total_cost)
|
assert(player.wallet.get_balance() == balance_before - total_cost)
|
||||||
assert(layout.get_next_backpack_cost() == -1)
|
assert(layout.get_next_backpack_cost() == -1)
|
||||||
|
|
@ -575,7 +582,10 @@ func _test_host_storage_purchase(
|
||||||
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
|
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
|
||||||
_shop_result.clear()
|
_shop_result.clear()
|
||||||
assert(not shop_service.request_cooler_capacity_upgrade().is_empty())
|
assert(not shop_service.request_cooler_capacity_upgrade().is_empty())
|
||||||
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
assert(
|
||||||
|
not _shop_result.is_empty() and bool(_shop_result[1]),
|
||||||
|
"Cooler purchase failed: %s" % [_shop_result],
|
||||||
|
)
|
||||||
assert(capacity.get_capacity() == expected_capacity)
|
assert(capacity.get_capacity() == expected_capacity)
|
||||||
assert(player.wallet.get_balance() == balance_before - total_cost)
|
assert(player.wallet.get_balance() == balance_before - total_cost)
|
||||||
assert(capacity.get_next_cost() == -1)
|
assert(capacity.get_next_cost() == -1)
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,10 @@ func _run() -> void:
|
||||||
var hotbar_data: Dictionary = parsed["hotbar"]
|
var hotbar_data: Dictionary = parsed["hotbar"]
|
||||||
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||||
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
||||||
assert(int(parsed["save_version"]) == 10)
|
assert(int(parsed["save_version"]) == 11)
|
||||||
|
assert(not PlayerHomeState.sanitize_save_data(
|
||||||
|
parsed.get("home", {})
|
||||||
|
).is_empty())
|
||||||
assert(
|
assert(
|
||||||
int(parsed["experience"]["total_experience"])
|
int(parsed["experience"]["total_experience"])
|
||||||
== 125
|
== 125
|
||||||
|
|
@ -197,8 +200,8 @@ func _run() -> void:
|
||||||
var invalid_state: Dictionary = valid_state.duplicate(true)
|
var invalid_state: Dictionary = valid_state.duplicate(true)
|
||||||
invalid_state["display_scale"] = 1000.0
|
invalid_state["display_scale"] = 1000.0
|
||||||
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
||||||
assert(NetworkProtocol.PROTOCOL_VERSION == 11)
|
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
|
||||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17)
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
|
||||||
assert(
|
assert(
|
||||||
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||||
== "fish_quality_v1"
|
== "fish_quality_v1"
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ func _run() -> void:
|
||||||
_validate_collection_mastery()
|
_validate_collection_mastery()
|
||||||
_validate_version_four_migration()
|
_validate_version_four_migration()
|
||||||
_validate_fishing_protocol_v2()
|
_validate_fishing_protocol_v2()
|
||||||
assert(NetworkProtocol.PROTOCOL_VERSION == 11)
|
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
|
||||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17)
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
|
||||||
assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11)
|
assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11)
|
||||||
assert(
|
assert(
|
||||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY
|
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY
|
||||||
|
|
@ -485,7 +485,7 @@ func _validate_version_four_migration() -> void:
|
||||||
version_four,
|
version_four,
|
||||||
4,
|
4,
|
||||||
)
|
)
|
||||||
assert(int(migrated.get("save_version", -1)) == 10)
|
assert(int(migrated.get("save_version", -1)) == 11)
|
||||||
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||||
assert(
|
assert(
|
||||||
is_equal_approx(
|
is_equal_approx(
|
||||||
|
|
@ -509,4 +509,7 @@ func _validate_version_four_migration() -> void:
|
||||||
assert(
|
assert(
|
||||||
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
|
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
|
||||||
)
|
)
|
||||||
|
assert(not PlayerHomeState.sanitize_save_data(
|
||||||
|
migrated.get("home", {})
|
||||||
|
).is_empty())
|
||||||
manager.queue_free()
|
manager.queue_free()
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,15 @@ func _run() -> void:
|
||||||
assert(bool(water_recovery.get("_prior_movement_enabled")))
|
assert(bool(water_recovery.get("_prior_movement_enabled")))
|
||||||
assert(bool(water_recovery.get("_prior_camera_input_enabled")))
|
assert(bool(water_recovery.get("_prior_camera_input_enabled")))
|
||||||
assert(not pause_menu.visible)
|
assert(not pause_menu.visible)
|
||||||
|
var test_world := main.get_node("TestWorld") as TestWorld
|
||||||
|
assert(test_world != null)
|
||||||
|
var recovery_entry := water_recovery.get("_entry_position") as Vector3
|
||||||
|
var resolved_recovery := water_recovery.call(
|
||||||
|
"_resolve_recovery_position"
|
||||||
|
) as Vector3
|
||||||
|
assert(resolved_recovery.is_equal_approx(
|
||||||
|
test_world.get_water_recovery_position(recovery_entry)
|
||||||
|
))
|
||||||
water_recovery.call("_finish_recovery")
|
water_recovery.call("_finish_recovery")
|
||||||
assert(player.is_movement_enabled())
|
assert(player.is_movement_enabled())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,38 @@ func _run_host() -> void:
|
||||||
var spawn_service := main.get_node(
|
var spawn_service := main.get_node(
|
||||||
"%PlayerSpawnService"
|
"%PlayerSpawnService"
|
||||||
) as PlayerSpawnService
|
) as PlayerSpawnService
|
||||||
|
var network_home := main.get_node(
|
||||||
|
"%NetworkHomeService"
|
||||||
|
) as NetworkHomeService
|
||||||
|
var home_world := main.get_node("%HomeWorldService") as HomeWorldService
|
||||||
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||||
assert(remote_avatar != null)
|
assert(remote_avatar != null)
|
||||||
assert(remote_avatar.bag.get_quantity(&"worms") == 0)
|
assert(remote_avatar.bag.get_quantity(&"worms") == 0)
|
||||||
remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1)
|
var remote_record := session.get_peer_record(remote_peer_id)
|
||||||
var remote_visuals := remote_avatar.get_node("Visuals") as Node3D
|
assert(remote_record != null)
|
||||||
remote_visuals.rotation.y = PI * 0.5
|
assert(await _wait_for_home(
|
||||||
|
network_home, remote_record.identity_fingerprint
|
||||||
|
))
|
||||||
|
var exterior_spawn: Transform3D = home_world.get_exterior_spawn_transform(
|
||||||
|
remote_record.identity_fingerprint
|
||||||
|
)
|
||||||
|
var exterior_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < exterior_deadline
|
||||||
|
and remote_avatar.global_position.distance_to(exterior_spawn.origin) > 0.25
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(remote_avatar.global_position.distance_to(exterior_spawn.origin) <= 0.25)
|
||||||
|
# Give the client enough local frames to observe the initial RV-door spawn
|
||||||
|
# before this test publishes its separate fishing-position teleport.
|
||||||
|
for _frame: int in 30:
|
||||||
|
await physics_frame
|
||||||
|
var cast_setup: Dictionary = await _find_clear_fishing_setup(
|
||||||
|
main, remote_avatar
|
||||||
|
)
|
||||||
|
assert(not cast_setup.is_empty(), "no clear fishing setup was found")
|
||||||
|
var expected_position: Vector3 = cast_setup["position"]
|
||||||
|
var expected_direction: Vector3 = cast_setup["direction"]
|
||||||
session.publish_authoritative_teleport(remote_peer_id)
|
session.publish_authoritative_teleport(remote_peer_id)
|
||||||
var aiming_deadline: int = Time.get_ticks_msec() + 15000
|
var aiming_deadline: int = Time.get_ticks_msec() + 15000
|
||||||
while (
|
while (
|
||||||
|
|
@ -82,7 +108,7 @@ func _run_host() -> void:
|
||||||
== Player.FishingVisualPhase.CASTING
|
== Player.FishingVisualPhase.CASTING
|
||||||
)
|
)
|
||||||
var remote_position_error := (
|
var remote_position_error := (
|
||||||
remote_avatar.global_position - Vector3(-0.5, 3.95, 2.1)
|
remote_avatar.global_position - expected_position
|
||||||
)
|
)
|
||||||
remote_position_error.y = 0.0
|
remote_position_error.y = 0.0
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -90,7 +116,7 @@ func _run_host() -> void:
|
||||||
"remote casting position drifted: %s" % remote_avatar.global_position
|
"remote casting position drifted: %s" % remote_avatar.global_position
|
||||||
)
|
)
|
||||||
assert(
|
assert(
|
||||||
remote_avatar.get_facing_direction().dot(Vector3.LEFT) > 0.99,
|
remote_avatar.get_facing_direction().dot(expected_direction) > 0.99,
|
||||||
"remote casting facing drifted: %s"
|
"remote casting facing drifted: %s"
|
||||||
% remote_avatar.get_facing_direction()
|
% remote_avatar.get_facing_direction()
|
||||||
)
|
)
|
||||||
|
|
@ -196,6 +222,23 @@ func _run_client() -> void:
|
||||||
joined = true
|
joined = true
|
||||||
break
|
break
|
||||||
assert(joined)
|
assert(joined)
|
||||||
|
var network_home := main.get_node(
|
||||||
|
"%NetworkHomeService"
|
||||||
|
) as NetworkHomeService
|
||||||
|
var home_world := main.get_node("%HomeWorldService") as HomeWorldService
|
||||||
|
var local_fingerprint: String = session.get_local_identity_fingerprint()
|
||||||
|
assert(await _wait_for_home(network_home, local_fingerprint))
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
var exterior_spawn: Transform3D = (
|
||||||
|
home_world.get_exterior_spawn_transform(local_fingerprint)
|
||||||
|
)
|
||||||
|
var home_spawn_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < home_spawn_deadline
|
||||||
|
and player.global_position.distance_to(exterior_spawn.origin) > 0.25
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(player.global_position.distance_to(exterior_spawn.origin) <= 0.25)
|
||||||
|
|
||||||
var service := main.get_node(
|
var service := main.get_node(
|
||||||
"%NetworkFishingService"
|
"%NetworkFishingService"
|
||||||
|
|
@ -206,7 +249,6 @@ func _run_client() -> void:
|
||||||
cast_rejections.append(message)
|
cast_rejections.append(message)
|
||||||
)
|
)
|
||||||
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
var player := main.get("_player") as Player
|
|
||||||
var item_catalog := main.get("item_catalog") as ItemCatalog
|
var item_catalog := main.get("item_catalog") as ItemCatalog
|
||||||
var worms: ItemData = item_catalog.get_item_by_id(&"worms")
|
var worms: ItemData = item_catalog.get_item_by_id(&"worms")
|
||||||
if player.bag.get_quantity(&"worms") == 0:
|
if player.bag.get_quantity(&"worms") == 0:
|
||||||
|
|
@ -217,15 +259,20 @@ func _run_client() -> void:
|
||||||
"Visuals/CharacterRig/AnimationPlayer"
|
"Visuals/CharacterRig/AnimationPlayer"
|
||||||
) as AnimationPlayer
|
) as AnimationPlayer
|
||||||
assert(character_animation_player.has_animation(&"retract_sit"))
|
assert(character_animation_player.has_animation(&"retract_sit"))
|
||||||
var placement_deadline: int = Time.get_ticks_msec() + 5000
|
# The host chooses and publishes a clear target after both RV manifests have
|
||||||
|
# settled. Waiting for that authoritative teleport also coordinates the
|
||||||
|
# animation-observation window between these two independent processes.
|
||||||
|
var placement_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < placement_deadline
|
Time.get_ticks_msec() < placement_deadline
|
||||||
and player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1))
|
and player.global_position.distance_to(exterior_spawn.origin) <= 1.0
|
||||||
> 0.25
|
|
||||||
):
|
):
|
||||||
await physics_frame
|
await physics_frame
|
||||||
assert(player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1)) <= 0.25)
|
assert(player.global_position.distance_to(exterior_spawn.origin) > 1.0)
|
||||||
assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99)
|
for _frame: int in 2:
|
||||||
|
await physics_frame
|
||||||
|
var expected_position: Vector3 = player.global_position
|
||||||
|
var expected_direction: Vector3 = player.get_facing_direction()
|
||||||
|
|
||||||
player.set_casting_visual()
|
player.set_casting_visual()
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -241,7 +288,10 @@ func _run_client() -> void:
|
||||||
assert(player.is_sitting())
|
assert(player.is_sitting())
|
||||||
for _frame: int in 15:
|
for _frame: int in 15:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
|
||||||
|
assert(fishing_spot.get("_active_player") == null)
|
||||||
fishing_spot.call("_begin_aiming", player)
|
fishing_spot.call("_begin_aiming", player)
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
|
||||||
assert(player.is_movement_enabled())
|
assert(player.is_movement_enabled())
|
||||||
assert(
|
assert(
|
||||||
int(player.get("_fishing_visual_phase"))
|
int(player.get("_fishing_visual_phase"))
|
||||||
|
|
@ -250,6 +300,7 @@ func _run_client() -> void:
|
||||||
var cast_charge: float = 0.0
|
var cast_charge: float = 0.0
|
||||||
var target: Vector3 = Vector3.ZERO
|
var target: Vector3 = Vector3.ZERO
|
||||||
var cast_origin: Vector3 = fishing_spot.get("_cast_origin_position")
|
var cast_origin: Vector3 = fishing_spot.get("_cast_origin_position")
|
||||||
|
var target_diagnostics: Array[String] = []
|
||||||
for charge_step: int in range(1, 21):
|
for charge_step: int in range(1, 21):
|
||||||
var candidate_charge := float(charge_step) / 20.0
|
var candidate_charge := float(charge_step) / 20.0
|
||||||
fishing_spot.set("_cast_charge", candidate_charge)
|
fishing_spot.set("_cast_charge", candidate_charge)
|
||||||
|
|
@ -258,14 +309,33 @@ func _run_client() -> void:
|
||||||
var candidate_surface := fishing_spot.get(
|
var candidate_surface := fishing_spot.get(
|
||||||
"_aim_surface_sample"
|
"_aim_surface_sample"
|
||||||
) as FishingSurfaceSample
|
) as FishingSurfaceSample
|
||||||
|
target_diagnostics.append(
|
||||||
|
"charge=%.2f surface=%s path=%s target=%s" % [
|
||||||
|
candidate_charge,
|
||||||
|
candidate_surface.is_fishable(),
|
||||||
|
fishing_spot.get("_cast_path_is_clear"),
|
||||||
|
candidate_target,
|
||||||
|
]
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
candidate_surface.is_fishable()
|
candidate_surface.is_fishable()
|
||||||
and fishing_spot.is_cast_path_clear(cast_origin, candidate_target)
|
and bool(fishing_spot.get("_cast_path_is_clear"))
|
||||||
):
|
):
|
||||||
cast_charge = candidate_charge
|
cast_charge = candidate_charge
|
||||||
target = candidate_target
|
target = candidate_target
|
||||||
break
|
break
|
||||||
assert(cast_charge > 0.0, "no clear fishable test target was found")
|
assert(
|
||||||
|
cast_charge > 0.0,
|
||||||
|
(
|
||||||
|
"no clear fishable test target was found; position=%s facing=%s "
|
||||||
|
+ "expected=%s diagnostics=%s"
|
||||||
|
) % [
|
||||||
|
player.global_position,
|
||||||
|
player.get_facing_direction(),
|
||||||
|
expected_direction,
|
||||||
|
target_diagnostics,
|
||||||
|
],
|
||||||
|
)
|
||||||
var evidence: Dictionary = fishing_spot.call("_build_network_evidence")
|
var evidence: Dictionary = fishing_spot.call("_build_network_evidence")
|
||||||
fishing_spot.set("state", FishingSpotType.FishingState.CASTING)
|
fishing_spot.set("state", FishingSpotType.FishingState.CASTING)
|
||||||
player.set_movement_enabled(false)
|
player.set_movement_enabled(false)
|
||||||
|
|
@ -388,3 +458,79 @@ func _create_initialized_main() -> Node:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
return main
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_home(
|
||||||
|
network_home: NetworkHomeService,
|
||||||
|
fingerprint: String,
|
||||||
|
) -> bool:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
var manifests: Dictionary = network_home.get(
|
||||||
|
"_manifests_by_fingerprint"
|
||||||
|
)
|
||||||
|
if manifests.has(fingerprint):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _find_clear_fishing_setup(main: Node, player: Player) -> Dictionary:
|
||||||
|
var world := main.get_node("TestWorld") as TestWorld
|
||||||
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
|
var regions: Array[FishableWaterRegion] = world.get_fishable_water_regions()
|
||||||
|
for region: FishableWaterRegion in regions:
|
||||||
|
var body := region.get_parent() as WaterBodyAuthoring
|
||||||
|
if body == null or body.surface_size.x <= 0.0 or body.surface_size.y <= 0.0:
|
||||||
|
continue
|
||||||
|
var center: Vector3 = region.global_position
|
||||||
|
var half_size: Vector2 = body.surface_size * 0.5
|
||||||
|
var candidate_offsets: Array[Vector3] = [
|
||||||
|
Vector3(half_size.x + 0.8, 0.0, 0.0),
|
||||||
|
Vector3(-half_size.x - 0.8, 0.0, 0.0),
|
||||||
|
Vector3(0.0, 0.0, half_size.y + 0.8),
|
||||||
|
Vector3(0.0, 0.0, -half_size.y - 0.8),
|
||||||
|
]
|
||||||
|
for offset: Vector3 in candidate_offsets:
|
||||||
|
var position := Vector3(
|
||||||
|
center.x + offset.x,
|
||||||
|
region.get_surface_height() + 1.44,
|
||||||
|
center.z + offset.z,
|
||||||
|
)
|
||||||
|
var direction: Vector3 = center - position
|
||||||
|
direction.y = 0.0
|
||||||
|
direction = direction.normalized()
|
||||||
|
player.global_position = position
|
||||||
|
var visuals := player.get_node("Visuals") as Node3D
|
||||||
|
visuals.global_rotation.y = atan2(-direction.x, -direction.z)
|
||||||
|
for _frame: int in 2:
|
||||||
|
await physics_frame
|
||||||
|
var cast_origin: Vector3 = player.get_cast_origin_position()
|
||||||
|
var launch_position: Vector3 = player.get_fishing_rod_tip().global_position
|
||||||
|
for charge_step: int in range(1, 21):
|
||||||
|
var cast_charge := float(charge_step) / 20.0
|
||||||
|
var cast_distance := lerpf(
|
||||||
|
fishing_spot.minimum_cast_distance,
|
||||||
|
fishing_spot.maximum_cast_distance,
|
||||||
|
cast_charge,
|
||||||
|
)
|
||||||
|
var query_position := cast_origin + direction * cast_distance
|
||||||
|
var sample: FishingSurfaceSample = (
|
||||||
|
fishing_spot.resolve_fishing_surface(
|
||||||
|
query_position, cast_origin.y
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var target: Vector3 = sample.get_bobber_position(
|
||||||
|
fishing_spot.solid_bobber_clearance
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
sample.is_fishable()
|
||||||
|
and fishing_spot.is_cast_path_clear(
|
||||||
|
launch_position, target
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"position": player.global_position,
|
||||||
|
"direction": direction,
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,13 @@ func _run() -> void:
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||||
|
)))
|
||||||
|
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
|
||||||
assert(session.start_private_host(TEST_PORT))
|
assert(session.start_private_host(TEST_PORT))
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(save_manager.initialize_new_game())
|
assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
for _frame: int in 8:
|
for _frame: int in 8:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
@ -38,7 +42,7 @@ func _run() -> void:
|
||||||
and game_ui != null
|
and game_ui != null
|
||||||
and catalog != null
|
and catalog != null
|
||||||
)
|
)
|
||||||
fishing_spot.minimum_showcase_duration = 0.15
|
fishing_spot.minimum_showcase_duration = 1.0
|
||||||
assert(player.bag.add_item(&"crab_net"))
|
assert(player.bag.add_item(&"crab_net"))
|
||||||
assert(player.hotbar.assign_item(0, &"crab_net"))
|
assert(player.hotbar.assign_item(0, &"crab_net"))
|
||||||
assert(player.hotbar.select_slot(0))
|
assert(player.hotbar.select_slot(0))
|
||||||
|
|
@ -81,7 +85,7 @@ func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(fishing_spot.get("_pending_catch") == crab_catch)
|
assert(fishing_spot.get("_pending_catch") == crab_catch)
|
||||||
Input.action_release("fish_primary")
|
Input.action_release("fish_primary")
|
||||||
await create_timer(0.2).timeout
|
fishing_spot.set("_showcase_input_lock_remaining", 0.0)
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(fishing_spot.get("_put_away_press_armed")))
|
assert(bool(fishing_spot.get("_put_away_press_armed")))
|
||||||
Input.parse_input_event(pocket_event)
|
Input.parse_input_event(pocket_event)
|
||||||
|
|
|
||||||
|
|
@ -536,6 +536,11 @@ func _validate_generated_region(
|
||||||
-INF,
|
-INF,
|
||||||
0.35,
|
0.35,
|
||||||
)
|
)
|
||||||
|
_validate_water_recovery_positions(
|
||||||
|
region,
|
||||||
|
grass_prop_surface_triangles,
|
||||||
|
sand_prop_surface_triangles,
|
||||||
|
)
|
||||||
var anchors := region.get_node(
|
var anchors := region.get_node(
|
||||||
"GatherableAnchors/ReachableTreeTrunks"
|
"GatherableAnchors/ReachableTreeTrunks"
|
||||||
) as GatherableAnchorSet3D
|
) as GatherableAnchorSet3D
|
||||||
|
|
@ -1268,6 +1273,76 @@ func _triangle_surface_height_at(
|
||||||
return highest
|
return highest
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_water_recovery_positions(
|
||||||
|
region: GeneratedWorldRegion,
|
||||||
|
grass_triangles: Array[PackedVector3Array],
|
||||||
|
sand_triangles: Array[PackedVector3Array],
|
||||||
|
) -> void:
|
||||||
|
var fresh_water_root := region.get_node(
|
||||||
|
"WaterBodies/FreshWaterBodies"
|
||||||
|
) as Node3D
|
||||||
|
assert(fresh_water_root != null and fresh_water_root.get_child_count() > 0)
|
||||||
|
var fresh_water := fresh_water_root.get_child(0) as WaterBodyAuthoring
|
||||||
|
assert(fresh_water != null)
|
||||||
|
var entry_position := fresh_water.global_position
|
||||||
|
var fallback_position := region.get_player_spawn_transform().origin
|
||||||
|
var recovery_position := region.get_water_recovery_position(
|
||||||
|
entry_position,
|
||||||
|
fallback_position,
|
||||||
|
)
|
||||||
|
assert(recovery_position.is_finite())
|
||||||
|
assert(
|
||||||
|
_horizontal_distance_squared(entry_position, recovery_position)
|
||||||
|
< _horizontal_distance_squared(entry_position, fallback_position),
|
||||||
|
"Fresh-water recovery returned the distant generated-world spawn.",
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
recovery_position.y
|
||||||
|
> GeneratedWorldRegion.WATER_HEIGHT
|
||||||
|
+ GeneratedWorldRegion.WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE
|
||||||
|
)
|
||||||
|
var recovery_triangles: Array[PackedVector3Array] = []
|
||||||
|
recovery_triangles.append_array(grass_triangles)
|
||||||
|
recovery_triangles.append_array(sand_triangles)
|
||||||
|
var center_height := _triangle_surface_height_at(
|
||||||
|
recovery_position,
|
||||||
|
recovery_triangles,
|
||||||
|
)
|
||||||
|
assert(center_height > -INF)
|
||||||
|
assert(absf(center_height - recovery_position.y) <= 0.01)
|
||||||
|
for direction: Vector2 in GeneratedWorldRegion.PROP_SURFACE_SAMPLE_DIRECTIONS:
|
||||||
|
var footprint_position := recovery_position + Vector3(
|
||||||
|
direction.x * GeneratedWorldRegion.WATER_RECOVERY_FOOTPRINT_RADIUS,
|
||||||
|
0.0,
|
||||||
|
direction.y * GeneratedWorldRegion.WATER_RECOVERY_FOOTPRINT_RADIUS,
|
||||||
|
)
|
||||||
|
var footprint_height := _triangle_surface_height_at(
|
||||||
|
footprint_position,
|
||||||
|
recovery_triangles,
|
||||||
|
)
|
||||||
|
assert(footprint_height > -INF)
|
||||||
|
assert(
|
||||||
|
absf(footprint_height - recovery_position.y)
|
||||||
|
<= GeneratedWorldRegion.PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
region.get_water_recovery_position(
|
||||||
|
entry_position,
|
||||||
|
fallback_position,
|
||||||
|
).is_equal_approx(recovery_position)
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
region.get_water_recovery_position(
|
||||||
|
Vector3(INF, 0.0, 0.0),
|
||||||
|
fallback_position,
|
||||||
|
).is_equal_approx(fallback_position)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _horizontal_distance_squared(a: Vector3, b: Vector3) -> float:
|
||||||
|
return Vector2(a.x - b.x, a.z - b.z).length_squared()
|
||||||
|
|
||||||
|
|
||||||
func _validate_tree_gatherable_anchors(
|
func _validate_tree_gatherable_anchors(
|
||||||
region: GeneratedWorldRegion,
|
region: GeneratedWorldRegion,
|
||||||
decorations: Node3D,
|
decorations: Node3D,
|
||||||
|
|
|
||||||
178
tests/home_spawn_multiplayer_validation.gd
Normal file
178
tests/home_spawn_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||||
|
const TEST_PORT: int = 18222
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||||
|
if arguments.has("host"):
|
||||||
|
await _run_host()
|
||||||
|
return
|
||||||
|
if arguments.has("client"):
|
||||||
|
await _run_client()
|
||||||
|
return
|
||||||
|
push_error("Home spawn validation needs host or client mode.")
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_host() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var network_home := main.get_node(
|
||||||
|
"%NetworkHomeService"
|
||||||
|
) as NetworkHomeService
|
||||||
|
var home_world := main.get_node("%HomeWorldService") as HomeWorldService
|
||||||
|
var spawn_service := main.get_node(
|
||||||
|
"%PlayerSpawnService"
|
||||||
|
) as PlayerSpawnService
|
||||||
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||||
|
)))
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_prepare_host_world", WorldLayout.STARTER_ISLAND, 1
|
||||||
|
)))
|
||||||
|
assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
|
main.call("_enter_gameplay")
|
||||||
|
assert(session.set_host_open(true))
|
||||||
|
|
||||||
|
var host_fingerprint: String = session.get_local_identity_fingerprint()
|
||||||
|
assert(await _wait_for_home(network_home, host_fingerprint))
|
||||||
|
var host_avatar: Player = spawn_service.get_avatar(1)
|
||||||
|
var host_spawn: Transform3D = home_world.get_exterior_spawn_transform(
|
||||||
|
host_fingerprint
|
||||||
|
)
|
||||||
|
assert(_same_door_position(host_avatar.global_position, host_spawn.origin))
|
||||||
|
assert(network_home.request_enter(host_fingerprint))
|
||||||
|
await physics_frame
|
||||||
|
host_avatar.global_position = home_world.get_interior_exit_position(
|
||||||
|
host_fingerprint
|
||||||
|
)
|
||||||
|
assert(network_home.request_exit())
|
||||||
|
await physics_frame
|
||||||
|
assert(_same_door_position(host_avatar.global_position, host_spawn.origin))
|
||||||
|
|
||||||
|
var remote_peer_id: int = await _wait_for_remote_peer(session)
|
||||||
|
assert(remote_peer_id > 1)
|
||||||
|
var remote_record := session.get_peer_record(remote_peer_id)
|
||||||
|
var remote_fingerprint: String = remote_record.identity_fingerprint
|
||||||
|
assert(await _wait_for_home(network_home, remote_fingerprint))
|
||||||
|
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||||
|
var remote_spawn: Transform3D = home_world.get_exterior_spawn_transform(
|
||||||
|
remote_fingerprint
|
||||||
|
)
|
||||||
|
assert(_same_door_position(
|
||||||
|
remote_avatar.global_position, remote_spawn.origin
|
||||||
|
))
|
||||||
|
|
||||||
|
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
|
and session.is_authenticated_peer(remote_peer_id)
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(not session.is_authenticated_peer(remote_peer_id))
|
||||||
|
print("Home spawn multiplayer host validation: PASS")
|
||||||
|
await _cleanup(main, session)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_client() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
main.call(
|
||||||
|
"_on_title_join_game_requested",
|
||||||
|
"127.0.0.1:%d" % TEST_PORT,
|
||||||
|
)
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var network_home := main.get_node(
|
||||||
|
"%NetworkHomeService"
|
||||||
|
) as NetworkHomeService
|
||||||
|
var home_world := main.get_node("%HomeWorldService") as HomeWorldService
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||||
|
main.call("_confirm_server_trust")
|
||||||
|
if session.is_joined_client() and bool(main.get("_gameplay_started")):
|
||||||
|
break
|
||||||
|
assert(session.is_joined_client())
|
||||||
|
var fingerprint: String = session.get_local_identity_fingerprint()
|
||||||
|
assert(await _wait_for_home(network_home, fingerprint))
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
var exterior_spawn: Transform3D = (
|
||||||
|
home_world.get_exterior_spawn_transform(fingerprint)
|
||||||
|
)
|
||||||
|
deadline = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < deadline
|
||||||
|
and not _same_door_position(
|
||||||
|
player.global_position, exterior_spawn.origin
|
||||||
|
)
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(_same_door_position(
|
||||||
|
player.global_position, exterior_spawn.origin
|
||||||
|
))
|
||||||
|
print("Home spawn multiplayer client validation: PASS")
|
||||||
|
await _cleanup(main, session)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_initialized_main() -> Node:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main: Node = MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_home(
|
||||||
|
network_home: NetworkHomeService,
|
||||||
|
fingerprint: String,
|
||||||
|
) -> bool:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
var manifests: Dictionary = network_home.get(
|
||||||
|
"_manifests_by_fingerprint"
|
||||||
|
)
|
||||||
|
if manifests.has(fingerprint):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_remote_peer(session: NetworkSession) -> int:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
for peer_id: int in session.get_authenticated_peer_ids():
|
||||||
|
if peer_id != session.get_local_peer_id():
|
||||||
|
return peer_id
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
func _same_door_position(first: Vector3, second: Vector3) -> bool:
|
||||||
|
return (
|
||||||
|
Vector2(first.x, first.z).distance_to(Vector2(second.x, second.z))
|
||||||
|
< 0.08
|
||||||
|
and absf(first.y - second.y) < 0.3
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _cleanup(main: Node, session: NetworkSession) -> void:
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit()
|
||||||
1
tests/home_spawn_multiplayer_validation.gd.uid
Normal file
1
tests/home_spawn_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bndtkip5p6xma
|
||||||
|
|
@ -21,13 +21,17 @@ func _run() -> void:
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
assert(session != null)
|
assert(session != null)
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||||
|
)))
|
||||||
|
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
|
||||||
var occupied_port := PacketPeerUDP.new()
|
var occupied_port := PacketPeerUDP.new()
|
||||||
assert(occupied_port.bind(18137, "127.0.0.1") == OK)
|
assert(occupied_port.bind(18137, "127.0.0.1") == OK)
|
||||||
assert(session.start_private_host(18137, 3))
|
assert(session.start_private_host(18137, 3))
|
||||||
assert(session.get_host_port() == 18138)
|
assert(session.get_host_port() == 18138)
|
||||||
occupied_port.close()
|
occupied_port.close()
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(save_manager.initialize_new_game())
|
assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
await physics_frame
|
await physics_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
@ -205,8 +209,11 @@ func _run() -> void:
|
||||||
)
|
)
|
||||||
assert(bool(decoded.get("ok", false)))
|
assert(bool(decoded.get("ok", false)))
|
||||||
var save_data: Dictionary = decoded["data"]
|
var save_data: Dictionary = decoded["data"]
|
||||||
assert(int(save_data.get("save_version", -1)) == 10)
|
assert(int(save_data.get("save_version", -1)) == 11)
|
||||||
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
||||||
|
assert(not PlayerHomeState.sanitize_save_data(
|
||||||
|
save_data.get("home", {})
|
||||||
|
).is_empty())
|
||||||
|
|
||||||
_validate_pause_session_switch(main, session)
|
_validate_pause_session_switch(main, session)
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,12 @@ func _run_first_client() -> void:
|
||||||
player.toggle_sitting()
|
player.toggle_sitting()
|
||||||
assert(player.is_sitting())
|
assert(player.is_sitting())
|
||||||
assert(player.bag.add_item(&"crab_net"))
|
assert(player.bag.add_item(&"crab_net"))
|
||||||
assert(player.hotbar.assign_item(0, &"crab_net"))
|
# Slot zero begins selected with the starter rod. Replacing that slot does
|
||||||
assert(player.hotbar.select_slot(0))
|
# not emit a selection change, so use the next slot to exercise the same
|
||||||
for _frame: int in 2:
|
# equipped-item path a player uses in normal gameplay.
|
||||||
await process_frame
|
assert(player.hotbar.assign_item(1, &"crab_net"))
|
||||||
assert(bool(player.get("_active_item_is_net")))
|
assert(player.hotbar.select_slot(1))
|
||||||
|
assert(await _wait_for_local_net(main, player))
|
||||||
assert(player.begin_animation_action(ACTIVE_ACTION))
|
assert(player.begin_animation_action(ACTIVE_ACTION))
|
||||||
await create_timer(0.5).timeout
|
await create_timer(0.5).timeout
|
||||||
assert(StringName(str(player.get("_animation_action_id"))) == ACTIVE_ACTION)
|
assert(StringName(str(player.get("_animation_action_id"))) == ACTIVE_ACTION)
|
||||||
|
|
@ -185,6 +186,29 @@ func _wait_for_registry_size(session: NetworkSession, expected: int) -> bool:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_local_net(main: Node, player: Player) -> bool:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
# A joined client can finish its world transition one frame after the
|
||||||
|
# hotbar selection signal. Refresh through the production path until the
|
||||||
|
# ready fishing state accepts the equipped item.
|
||||||
|
main.call("_refresh_active_hotbar_item")
|
||||||
|
await process_frame
|
||||||
|
if bool(player.get("_active_item_is_net")):
|
||||||
|
return true
|
||||||
|
var fishing_spot: Node = main.get_node("%FishingSpot")
|
||||||
|
print(
|
||||||
|
"net equip wait timed out: ",
|
||||||
|
{
|
||||||
|
"fishing_state": int(fishing_spot.get("state")),
|
||||||
|
"selected_slot": player.hotbar.get_selected_slot(),
|
||||||
|
"selected_item": String(player.hotbar.get_selected_item_id()),
|
||||||
|
"owns_net": player.bag.owns_item(&"crab_net"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _history_contains(service: NetworkChatService, body: String) -> bool:
|
func _history_contains(service: NetworkChatService, body: String) -> bool:
|
||||||
return service.get_history().any(
|
return service.get_history().any(
|
||||||
func(message: Dictionary) -> bool:
|
func(message: Dictionary) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ extends SceneTree
|
||||||
|
|
||||||
const MainScene = preload("res://main/main.tscn")
|
const MainScene = preload("res://main/main.tscn")
|
||||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const MENU_TRANSITION_SETTLE_SECONDS := 3.0
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
|
|
@ -36,12 +37,16 @@ func _run() -> void:
|
||||||
var logbook := player_menu.get_node("%CatalogLogbook") as LogbookPage
|
var logbook := player_menu.get_node("%CatalogLogbook") as LogbookPage
|
||||||
var backdrop := main.get_node("%PlayerMenuBackdrop") as ColorRect
|
var backdrop := main.get_node("%PlayerMenuBackdrop") as ColorRect
|
||||||
var hotbar := game_ui.get_node("%Hotbar") as Control
|
var hotbar := game_ui.get_node("%Hotbar") as Control
|
||||||
|
var fishing_spot := main.get("_fishing_spot") as FishingSpot
|
||||||
|
assert(logbook.get("_fishing_spot") == fishing_spot)
|
||||||
|
assert(logbook.get("_world_time") == main.get("_world_time"))
|
||||||
|
assert(logbook.get("_world_weather") == main.get("_world_weather"))
|
||||||
|
|
||||||
var shortcut := InputEventKey.new()
|
var shortcut := InputEventKey.new()
|
||||||
shortcut.pressed = true
|
shortcut.pressed = true
|
||||||
shortcut.physical_keycode = KEY_L
|
shortcut.physical_keycode = KEY_L
|
||||||
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
|
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
|
||||||
await create_timer(2.2).timeout
|
await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
|
||||||
assert(player_menu.visible)
|
assert(player_menu.visible)
|
||||||
assert(logbook.visible)
|
assert(logbook.visible)
|
||||||
assert(backdrop.visible)
|
assert(backdrop.visible)
|
||||||
|
|
@ -51,6 +56,18 @@ func _run() -> void:
|
||||||
var entry_buttons: Dictionary = logbook.get("_entry_buttons")
|
var entry_buttons: Dictionary = logbook.get("_entry_buttons")
|
||||||
assert(entry_buttons.size() == 108)
|
assert(entry_buttons.size() == 108)
|
||||||
await _capture_if_requested("-unknown")
|
await _capture_if_requested("-unknown")
|
||||||
|
logbook.call("_set_index_open", true, false)
|
||||||
|
await process_frame
|
||||||
|
assert(bool(logbook.get("_index_open")))
|
||||||
|
assert((logbook.get("_index_body") as Control).visible)
|
||||||
|
await _capture_if_requested("-index")
|
||||||
|
logbook.set_compact_presentation(true)
|
||||||
|
await process_frame
|
||||||
|
await _capture_if_requested("-index-compact")
|
||||||
|
logbook.set_compact_presentation(false)
|
||||||
|
await process_frame
|
||||||
|
logbook.call("_set_index_open", false, false)
|
||||||
|
await process_frame
|
||||||
logbook.call(
|
logbook.call(
|
||||||
"_select_category", WaterType.Type.FRESH_WATER
|
"_select_category", WaterType.Type.FRESH_WATER
|
||||||
)
|
)
|
||||||
|
|
@ -68,13 +85,29 @@ func _run() -> void:
|
||||||
logbook.call("_select_entry", &"bluegill", &"bluegill")
|
logbook.call("_select_entry", &"bluegill", &"bluegill")
|
||||||
await create_timer(0.2).timeout
|
await create_timer(0.2).timeout
|
||||||
await _capture_if_requested("-known")
|
await _capture_if_requested("-known")
|
||||||
|
var bluegill := (
|
||||||
|
main.get("fish_catalog") as FishPool
|
||||||
|
).get_fish_by_id(&"bluegill")
|
||||||
|
var bluegill_available_now := fishing_spot.is_species_available_now(bluegill)
|
||||||
|
var index_options := logbook.get("_index_options") as Dictionary
|
||||||
|
index_options["availability"] = (
|
||||||
|
LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW
|
||||||
|
)
|
||||||
|
logbook.notify_availability_context_changed()
|
||||||
|
await process_frame
|
||||||
|
assert(
|
||||||
|
(logbook.get("_entry_buttons") as Dictionary).has(&"bluegill")
|
||||||
|
== bluegill_available_now
|
||||||
|
)
|
||||||
|
logbook.call("_reset_index")
|
||||||
|
await process_frame
|
||||||
|
|
||||||
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
|
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
|
||||||
await create_timer(2.2).timeout
|
await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
|
||||||
assert(not player_menu.visible)
|
assert(not player_menu.visible)
|
||||||
|
|
||||||
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
|
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
|
||||||
await create_timer(2.2).timeout
|
await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
|
||||||
player_menu.call(
|
player_menu.call(
|
||||||
"_show_section_immediate", PlayerMenu.Section.PROFILE
|
"_show_section_immediate", PlayerMenu.Section.PROFILE
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ const CatalogResource: FishPoolType = preload(
|
||||||
"res://fish/pools/fish_catalog.tres"
|
"res://fish/pools/fish_catalog.tres"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _query_available_id: StringName
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
call_deferred("_run")
|
call_deferred("_run")
|
||||||
|
|
@ -35,6 +37,7 @@ func _run() -> void:
|
||||||
quit()
|
quit()
|
||||||
return
|
return
|
||||||
_validate_catalog()
|
_validate_catalog()
|
||||||
|
_validate_catalog_query()
|
||||||
await _validate_page()
|
await _validate_page()
|
||||||
print("Logbook validation: PASS")
|
print("Logbook validation: PASS")
|
||||||
quit()
|
quit()
|
||||||
|
|
@ -68,6 +71,348 @@ func _validate_catalog() -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_catalog_query() -> void:
|
||||||
|
var collection := CollectionLogType.new()
|
||||||
|
var inventory := FishInventoryType.new()
|
||||||
|
var fresh_species: Array[FishDataType] = []
|
||||||
|
for fish: FishDataType in LogbookCatalog.ordered_species(
|
||||||
|
CatalogResource.candidates
|
||||||
|
):
|
||||||
|
if LogbookCatalog.category_for(fish) == LogbookCatalog.Category.FRESH_WATER:
|
||||||
|
fresh_species.append(fish)
|
||||||
|
assert(fresh_species.size() == 108)
|
||||||
|
var first := fresh_species[0]
|
||||||
|
var second := fresh_species[1]
|
||||||
|
var unknown := fresh_species[2]
|
||||||
|
for quality: int in FishQualityType.TIER_COUNT:
|
||||||
|
collection.record_catch(first.id, quality)
|
||||||
|
collection.record_catch(second.id, FishQualityType.Tier.BORING)
|
||||||
|
var held := _make_test_catch(first, true)
|
||||||
|
assert(inventory.add_catch(held))
|
||||||
|
|
||||||
|
var options := _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
|
||||||
|
var filtered := LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered.size() == 2)
|
||||||
|
assert(first in filtered and second in filtered)
|
||||||
|
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered.size() == fresh_species.size() - 2)
|
||||||
|
options["sort"] = LogbookCatalog.SortMode.CATALOG_NUMBER
|
||||||
|
options["descending"] = true
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered.front() == fresh_species.back())
|
||||||
|
assert(filtered.back() == unknown)
|
||||||
|
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.INCOMPLETE
|
||||||
|
options["descending"] = false
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered == [second])
|
||||||
|
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
|
||||||
|
options["sort"] = LogbookCatalog.SortMode.CATCH_COUNT
|
||||||
|
options["descending"] = true
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered == [first, second])
|
||||||
|
|
||||||
|
options = _default_query_options()
|
||||||
|
options["search"] = unknown.display_name
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered.is_empty())
|
||||||
|
options["search"] = str(unknown.catalog_number)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered == [unknown])
|
||||||
|
|
||||||
|
options = _default_query_options()
|
||||||
|
options["holding"] = LogbookCatalog.HoldingFilter.FAVORITED
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered == [first])
|
||||||
|
options["holding"] = LogbookCatalog.HoldingFilter.IN_COOLER
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered == [first])
|
||||||
|
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
|
||||||
|
options["group"] = String(first.collection_group)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(first in filtered)
|
||||||
|
for fish: FishDataType in filtered:
|
||||||
|
assert(fish.collection_group == first.collection_group)
|
||||||
|
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
|
||||||
|
options["rarity"] = (
|
||||||
|
int(first.rarity) + LogbookCatalog.RarityFilter.COMMON
|
||||||
|
)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(first in filtered)
|
||||||
|
for fish: FishDataType in filtered:
|
||||||
|
assert(fish.rarity == first.rarity)
|
||||||
|
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
|
||||||
|
options["method"] = (
|
||||||
|
int(first.collection_method) + LogbookCatalog.MethodFilter.FISHING
|
||||||
|
)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(first in filtered)
|
||||||
|
for fish: FishDataType in filtered:
|
||||||
|
assert(fish.collection_method == first.collection_method)
|
||||||
|
|
||||||
|
var first_season := -1
|
||||||
|
for season: int in 4:
|
||||||
|
if first.is_available_in_season(season):
|
||||||
|
first_season = season
|
||||||
|
break
|
||||||
|
assert(first_season >= 0)
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
|
||||||
|
options["season"] = (
|
||||||
|
first_season + LogbookCatalog.SeasonFilter.SPRING
|
||||||
|
)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(first in filtered)
|
||||||
|
for fish: FishDataType in filtered:
|
||||||
|
assert(fish.is_available_in_season(first_season))
|
||||||
|
|
||||||
|
var first_is_available_by_day := (
|
||||||
|
first.availability == null or first.availability.allow_day
|
||||||
|
)
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
|
||||||
|
options["time"] = (
|
||||||
|
LogbookCatalog.TimeFilter.DAY
|
||||||
|
if first_is_available_by_day
|
||||||
|
else LogbookCatalog.TimeFilter.NIGHT
|
||||||
|
)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(first in filtered)
|
||||||
|
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
|
||||||
|
options["sort"] = LogbookCatalog.SortMode.QUALITY_PROGRESS
|
||||||
|
options["descending"] = true
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered == [first, second])
|
||||||
|
for sort_mode: LogbookCatalog.SortMode in [
|
||||||
|
LogbookCatalog.SortMode.NAME,
|
||||||
|
LogbookCatalog.SortMode.RARITY,
|
||||||
|
LogbookCatalog.SortMode.VALUE,
|
||||||
|
LogbookCatalog.SortMode.WEIGHT,
|
||||||
|
]:
|
||||||
|
options = _default_query_options()
|
||||||
|
options["sort"] = sort_mode
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(filtered[0] in [first, second])
|
||||||
|
assert(filtered[1] in [first, second])
|
||||||
|
assert(filtered[0] != filtered[1])
|
||||||
|
assert(filtered[2] == unknown)
|
||||||
|
|
||||||
|
_query_available_id = second.id
|
||||||
|
options = _default_query_options()
|
||||||
|
options["availability"] = (
|
||||||
|
LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW
|
||||||
|
)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
Callable(self, "_query_species_available_now"),
|
||||||
|
)
|
||||||
|
assert(filtered == [second])
|
||||||
|
|
||||||
|
_query_available_id = unknown.id
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
|
||||||
|
options["rarity"] = (
|
||||||
|
int(unknown.rarity) + LogbookCatalog.RarityFilter.COMMON
|
||||||
|
)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
Callable(self, "_query_species_available_now"),
|
||||||
|
)
|
||||||
|
assert(filtered == [unknown])
|
||||||
|
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
|
||||||
|
options["group"] = String(unknown.collection_group)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(unknown in filtered)
|
||||||
|
assert(
|
||||||
|
String(unknown.collection_group)
|
||||||
|
in LogbookCatalog.available_groups(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var unknown_is_available_by_day := (
|
||||||
|
unknown.availability == null or unknown.availability.allow_day
|
||||||
|
)
|
||||||
|
options = _default_query_options()
|
||||||
|
options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
|
||||||
|
options["time"] = (
|
||||||
|
LogbookCatalog.TimeFilter.DAY
|
||||||
|
if unknown_is_available_by_day
|
||||||
|
else LogbookCatalog.TimeFilter.NIGHT
|
||||||
|
)
|
||||||
|
filtered = LogbookCatalog.filtered_species(
|
||||||
|
CatalogResource.candidates,
|
||||||
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
|
collection,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
assert(unknown in filtered)
|
||||||
|
inventory.free()
|
||||||
|
collection.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _default_query_options() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"search": "",
|
||||||
|
"show": LogbookCatalog.DiscoveryFilter.ALL,
|
||||||
|
"sort": LogbookCatalog.SortMode.CATALOG_NUMBER,
|
||||||
|
"descending": false,
|
||||||
|
"availability": LogbookCatalog.AvailabilityFilter.ANY,
|
||||||
|
"season": LogbookCatalog.SeasonFilter.ANY,
|
||||||
|
"time": LogbookCatalog.TimeFilter.ANY,
|
||||||
|
"rarity": LogbookCatalog.RarityFilter.ANY,
|
||||||
|
"method": LogbookCatalog.MethodFilter.ANY,
|
||||||
|
"holding": LogbookCatalog.HoldingFilter.ANY,
|
||||||
|
"group": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _make_test_catch(fish: FishDataType, favorited: bool) -> FishCatchType:
|
||||||
|
var fish_catch := FishCatchType.new()
|
||||||
|
fish_catch.fish = fish
|
||||||
|
fish_catch.fish_id = fish.id
|
||||||
|
fish_catch.weight_lb = fish.get_minimum_weight()
|
||||||
|
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
fish_catch.quality = FishQualityType.Tier.BORING
|
||||||
|
fish_catch.sale_value = fish.get_sale_value_for_weight(fish_catch.weight_lb)
|
||||||
|
fish_catch.is_favorited = favorited
|
||||||
|
fish_catch.ensure_identity()
|
||||||
|
return fish_catch
|
||||||
|
|
||||||
|
|
||||||
|
func _query_species_available_now(fish: FishDataType) -> bool:
|
||||||
|
return fish != null and fish.id == _query_available_id
|
||||||
|
|
||||||
|
|
||||||
func _validate_page() -> void:
|
func _validate_page() -> void:
|
||||||
var collection := CollectionLogType.new()
|
var collection := CollectionLogType.new()
|
||||||
var inventory := FishInventoryType.new()
|
var inventory := FishInventoryType.new()
|
||||||
|
|
@ -91,6 +436,10 @@ func _validate_page() -> void:
|
||||||
assert(outer_margin.get_theme_constant("margin_right") == 160)
|
assert(outer_margin.get_theme_constant("margin_right") == 160)
|
||||||
assert(outer_margin.get_theme_constant("margin_bottom") == 106)
|
assert(outer_margin.get_theme_constant("margin_bottom") == 106)
|
||||||
assert(is_equal_approx(book.offset_top, 65.0))
|
assert(is_equal_approx(book.offset_top, 65.0))
|
||||||
|
page.call("_set_index_open", true, false)
|
||||||
|
await process_frame
|
||||||
|
_validate_index_content_bounds(page)
|
||||||
|
page.call("_set_index_open", false, false)
|
||||||
page.set_compact_presentation(false)
|
page.set_compact_presentation(false)
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(outer_margin.get_theme_constant("margin_left") == 52)
|
assert(outer_margin.get_theme_constant("margin_left") == 52)
|
||||||
|
|
@ -158,6 +507,39 @@ func _validate_page() -> void:
|
||||||
)
|
)
|
||||||
var initial_detail_body := page.get("_detail_body") as VBoxContainer
|
var initial_detail_body := page.get("_detail_body") as VBoxContainer
|
||||||
assert(not initial_detail_body.get_parent() is ScrollContainer)
|
assert(not initial_detail_body.get_parent() is ScrollContainer)
|
||||||
|
var index_tab := page.get("_index_tab") as Button
|
||||||
|
var index_body := page.get("_index_body") as VBoxContainer
|
||||||
|
assert(index_tab != null and index_body != null)
|
||||||
|
assert(index_tab.size == LogbookPage.INDEX_TAB_SIZE)
|
||||||
|
assert(
|
||||||
|
not index_tab.get_global_rect().intersects(
|
||||||
|
(category_tabs.back() as Button).get_global_rect()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
page.call("_set_index_open", true, false)
|
||||||
|
await process_frame
|
||||||
|
assert(index_body.visible)
|
||||||
|
assert(not initial_detail_body.visible)
|
||||||
|
assert((page.get("_index_option_buttons") as Dictionary).size() == 10)
|
||||||
|
_validate_index_content_bounds(page)
|
||||||
|
page.call(
|
||||||
|
"_cycle_index_option",
|
||||||
|
&"show",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
await process_frame
|
||||||
|
assert((page.get("_entry_buttons") as Dictionary).is_empty())
|
||||||
|
assert(
|
||||||
|
(page.get("_empty_state") as Label).text
|
||||||
|
== "No entries match this index."
|
||||||
|
)
|
||||||
|
page.call("_reset_index")
|
||||||
|
await process_frame
|
||||||
|
assert((page.get("_entry_buttons") as Dictionary).size() == 108)
|
||||||
|
page.call("_set_index_open", false, false)
|
||||||
|
await process_frame
|
||||||
|
assert(not index_body.visible)
|
||||||
|
assert(initial_detail_body.visible)
|
||||||
assert(
|
assert(
|
||||||
page.call("_entry_label_text", "flathead catfish")
|
page.call("_entry_label_text", "flathead catfish")
|
||||||
== "flathead\ncatfish"
|
== "flathead\ncatfish"
|
||||||
|
|
@ -458,8 +840,19 @@ func _validate_page() -> void:
|
||||||
(page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
|
(page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
||||||
|
var live_index_options := page.get("_index_options") as Dictionary
|
||||||
|
live_index_options["holding"] = LogbookCatalog.HoldingFilter.IN_COOLER
|
||||||
|
page.call("_refresh_catalog")
|
||||||
|
await process_frame
|
||||||
|
assert(
|
||||||
|
(page.get("_entry_buttons") as Dictionary).keys()
|
||||||
|
== [&"bluegill"]
|
||||||
|
)
|
||||||
inventory.remove_catch_by_id(fish_catch.catch_id)
|
inventory.remove_catch_by_id(fish_catch.catch_id)
|
||||||
await process_frame
|
await process_frame
|
||||||
|
assert((page.get("_entry_buttons") as Dictionary).is_empty())
|
||||||
|
page.call("_reset_index")
|
||||||
|
await process_frame
|
||||||
assert(not _detail_text(page).contains("number owned"))
|
assert(not _detail_text(page).contains("number owned"))
|
||||||
page.queue_free()
|
page.queue_free()
|
||||||
collection.queue_free()
|
collection.queue_free()
|
||||||
|
|
@ -474,6 +867,54 @@ func _detail_text(page: LogbookPage) -> String:
|
||||||
return "\n".join(values)
|
return "\n".join(values)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_index_content_bounds(page: LogbookPage) -> void:
|
||||||
|
var body := page.get("_index_body") as VBoxContainer
|
||||||
|
assert(body != null and body.visible)
|
||||||
|
var right_page := body.get_parent() as Control
|
||||||
|
assert(right_page != null)
|
||||||
|
var page_rect := right_page.get_global_rect()
|
||||||
|
var body_rect := body.get_global_rect()
|
||||||
|
var protected_rect := Rect2(
|
||||||
|
page_rect.position + Vector2(
|
||||||
|
LogbookPage.INDEX_CONTENT_LEFT_INSET,
|
||||||
|
LogbookPage.INDEX_CONTENT_TOP_INSET,
|
||||||
|
),
|
||||||
|
page_rect.size - Vector2(
|
||||||
|
LogbookPage.INDEX_CONTENT_LEFT_INSET
|
||||||
|
+ LogbookPage.INDEX_CONTENT_RIGHT_INSET,
|
||||||
|
LogbookPage.INDEX_CONTENT_TOP_INSET
|
||||||
|
+ LogbookPage.INDEX_CONTENT_BOTTOM_INSET,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
body_rect.position.x
|
||||||
|
>= page_rect.position.x + LogbookPage.INDEX_CONTENT_LEFT_INSET - 1.0
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
body_rect.end.x
|
||||||
|
<= page_rect.end.x - LogbookPage.INDEX_CONTENT_RIGHT_INSET + 1.0
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
body_rect.position.y >= page_rect.position.y,
|
||||||
|
"Index starts above the authored page: body=%s page=%s"
|
||||||
|
% [body_rect, page_rect],
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
body_rect.end.y <= page_rect.end.y,
|
||||||
|
"Index extends below the authored page: body=%s page=%s"
|
||||||
|
% [body_rect, page_rect],
|
||||||
|
)
|
||||||
|
var controls: Array = page.get("_index_controls") as Array
|
||||||
|
for value: Variant in controls:
|
||||||
|
var control := value as Control
|
||||||
|
if control != null and control.visible:
|
||||||
|
assert(
|
||||||
|
protected_rect.grow(2.0).encloses(control.get_global_rect()),
|
||||||
|
"Index control crossed the authored right-page boundary: %s"
|
||||||
|
% control.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _descendant_label_text(parent: Node) -> String:
|
func _descendant_label_text(parent: Node) -> String:
|
||||||
var values: PackedStringArray = []
|
var values: PackedStringArray = []
|
||||||
for node: Node in parent.find_children("*", "Label", true, false):
|
for node: Node in parent.find_children("*", "Label", true, false):
|
||||||
|
|
|
||||||
|
|
@ -593,15 +593,22 @@ func _run_host() -> void:
|
||||||
var main: Node = await _create_initialized_main()
|
var main: Node = await _create_initialized_main()
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(session.set_host_world(
|
assert(bool(main.call(
|
||||||
|
"_apply_world",
|
||||||
WorldLayout.STARTER_ISLAND,
|
WorldLayout.STARTER_ISLAND,
|
||||||
NetworkProtocol.DEFAULT_WORLD_SEED,
|
NetworkProtocol.DEFAULT_WORLD_SEED,
|
||||||
))
|
true,
|
||||||
assert(session.start_private_host(TEST_PORT))
|
)))
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_prepare_host_world",
|
||||||
|
WorldLayout.STARTER_ISLAND,
|
||||||
|
NetworkProtocol.DEFAULT_WORLD_SEED,
|
||||||
|
)))
|
||||||
assert(save_manager.initialize_new_game(
|
assert(save_manager.initialize_new_game(
|
||||||
NetworkProtocol.DEFAULT_WORLD_SEED,
|
NetworkProtocol.DEFAULT_WORLD_SEED,
|
||||||
WorldLayout.STARTER_ISLAND,
|
WorldLayout.STARTER_ISLAND,
|
||||||
))
|
))
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
for _frame: int in 4:
|
for _frame: int in 4:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
@ -613,31 +620,40 @@ func _run_host() -> void:
|
||||||
# to settle before emitting the one-shot locomotion/action sequence.
|
# to settle before emitting the one-shot locomotion/action sequence.
|
||||||
await create_timer(3.0).timeout
|
await create_timer(3.0).timeout
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
|
var world := main.get_node("TestWorld") as TestWorld
|
||||||
player.configure_network_remote(true)
|
player.configure_network_remote(true)
|
||||||
player.apply_authoritative_network_input(_movement_input(1, true))
|
await _reset_locomotion_test_position(player, session, world)
|
||||||
await create_timer(2.5).timeout
|
var input_sequence: int = 1
|
||||||
player.apply_authoritative_network_input(_movement_input(2, false))
|
input_sequence = await _sustain_locomotion_input(
|
||||||
await create_timer(2.5).timeout
|
player, input_sequence, 1.5, true
|
||||||
player.apply_authoritative_network_input(
|
|
||||||
_movement_input(3, false, true, true)
|
|
||||||
)
|
)
|
||||||
await create_timer(2.0).timeout
|
await _reset_locomotion_test_position(player, session, world)
|
||||||
player.apply_authoritative_network_input(
|
input_sequence = await _sustain_locomotion_input(
|
||||||
_movement_input(4, false, false, true)
|
player, input_sequence, 1.5, false
|
||||||
)
|
)
|
||||||
await create_timer(2.0).timeout
|
await _reset_locomotion_test_position(player, session, world)
|
||||||
player.apply_authoritative_network_input(
|
input_sequence = await _sustain_locomotion_input(
|
||||||
_movement_input(5, false, false, true, &"draw", 1)
|
player, input_sequence, 1.5, false, true, true
|
||||||
)
|
)
|
||||||
|
input_sequence = await _sustain_locomotion_input(
|
||||||
|
player, input_sequence, 1.5, false, false, true
|
||||||
|
)
|
||||||
|
player.apply_authoritative_network_input(
|
||||||
|
_movement_input(input_sequence, false, false, true, &"draw", 1)
|
||||||
|
)
|
||||||
|
input_sequence += 1
|
||||||
await create_timer(1.0).timeout
|
await create_timer(1.0).timeout
|
||||||
player.apply_authoritative_network_input(
|
player.apply_authoritative_network_input(
|
||||||
_movement_input(6, false, false, true, &"strike", 2, true)
|
_movement_input(
|
||||||
|
input_sequence, false, false, true, &"strike", 2, true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
input_sequence += 1
|
||||||
await create_timer(2.0).timeout
|
await create_timer(2.0).timeout
|
||||||
player.apply_authoritative_network_input(
|
player.apply_authoritative_network_input(
|
||||||
_movement_input(7, false, false, false, &"", 3)
|
_movement_input(input_sequence, false, false, false, &"", 3)
|
||||||
)
|
)
|
||||||
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
|
var disconnect_deadline: int = Time.get_ticks_msec() + 15000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < disconnect_deadline
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
and session.is_authenticated_peer(remote_peer_id)
|
and session.is_authenticated_peer(remote_peer_id)
|
||||||
|
|
@ -686,12 +702,27 @@ func _run_client() -> void:
|
||||||
var saw_net_strike_paused: bool = false
|
var saw_net_strike_paused: bool = false
|
||||||
var saw_animation_advance: bool = false
|
var saw_animation_advance: bool = false
|
||||||
var saw_dust: bool = false
|
var saw_dust: bool = false
|
||||||
|
var observed_animations: Dictionary = {}
|
||||||
|
var observed_locomotion_states: Dictionary = {}
|
||||||
|
var maximum_sneak_speed: float = 0.0
|
||||||
var previous_animation: StringName = &""
|
var previous_animation: StringName = &""
|
||||||
var previous_animation_position: float = -1.0
|
var previous_animation_position: float = -1.0
|
||||||
var observation_deadline: int = Time.get_ticks_msec() + 18000
|
var observation_deadline: int = Time.get_ticks_msec() + 22000
|
||||||
while Time.get_ticks_msec() < observation_deadline:
|
while Time.get_ticks_msec() < observation_deadline:
|
||||||
await process_frame
|
await process_frame
|
||||||
var current_animation: StringName = animation_player.current_animation
|
var current_animation: StringName = animation_player.current_animation
|
||||||
|
observed_animations[current_animation] = true
|
||||||
|
var target_locomotion_state: int = int(host_avatar.get(
|
||||||
|
"_network_target_locomotion_state"
|
||||||
|
))
|
||||||
|
observed_locomotion_states[target_locomotion_state] = true
|
||||||
|
if target_locomotion_state == Player.LocomotionState.SNEAKING:
|
||||||
|
maximum_sneak_speed = maxf(
|
||||||
|
maximum_sneak_speed,
|
||||||
|
Vector2(
|
||||||
|
host_avatar.velocity.x, host_avatar.velocity.z
|
||||||
|
).length(),
|
||||||
|
)
|
||||||
saw_running = saw_running or current_animation.begins_with("running")
|
saw_running = saw_running or current_animation.begins_with("running")
|
||||||
saw_walking = saw_walking or current_animation.begins_with("walking")
|
saw_walking = saw_walking or current_animation.begins_with("walking")
|
||||||
saw_sneaking = saw_sneaking or current_animation == &"sneaking"
|
saw_sneaking = saw_sneaking or current_animation == &"sneaking"
|
||||||
|
|
@ -732,15 +763,44 @@ func _run_client() -> void:
|
||||||
and saw_dust
|
and saw_dust
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
assert(saw_running)
|
var missing_observations: Array[String] = []
|
||||||
assert(saw_walking)
|
if not saw_running:
|
||||||
assert(saw_sneaking)
|
missing_observations.append("running")
|
||||||
assert(saw_idle_sneak)
|
if not saw_walking:
|
||||||
assert(saw_net_draw)
|
missing_observations.append("walking")
|
||||||
assert(saw_net_strike)
|
if not saw_sneaking:
|
||||||
assert(saw_net_strike_paused)
|
missing_observations.append("sneaking")
|
||||||
assert(saw_animation_advance)
|
if not saw_idle_sneak:
|
||||||
assert(saw_dust)
|
missing_observations.append("idle_sneak")
|
||||||
|
if not saw_net_draw:
|
||||||
|
missing_observations.append("net_draw")
|
||||||
|
if not saw_net_strike:
|
||||||
|
missing_observations.append("net_strike")
|
||||||
|
if not saw_net_strike_paused:
|
||||||
|
missing_observations.append("net_strike_paused")
|
||||||
|
if not saw_animation_advance:
|
||||||
|
missing_observations.append("animation_advance")
|
||||||
|
if not saw_dust:
|
||||||
|
missing_observations.append("sprint_dust")
|
||||||
|
if not missing_observations.is_empty():
|
||||||
|
push_error(
|
||||||
|
(
|
||||||
|
"Missing movement observations %s; observed animations: %s; "
|
||||||
|
+ "locomotion states: %s; maximum sneak speed: %.3f"
|
||||||
|
) % [
|
||||||
|
missing_observations,
|
||||||
|
observed_animations.keys(),
|
||||||
|
observed_locomotion_states.keys(),
|
||||||
|
maximum_sneak_speed,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
print("Movement multiplayer client validation: PASS")
|
print("Movement multiplayer client validation: PASS")
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
|
|
@ -778,6 +838,39 @@ func _movement_input(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _sustain_locomotion_input(
|
||||||
|
player: Player,
|
||||||
|
starting_sequence: int,
|
||||||
|
duration_seconds: float,
|
||||||
|
sprinting: bool,
|
||||||
|
moving: bool = true,
|
||||||
|
sneaking: bool = false,
|
||||||
|
) -> int:
|
||||||
|
var sequence: int = starting_sequence
|
||||||
|
var deadline: int = (
|
||||||
|
Time.get_ticks_msec() + roundi(duration_seconds * 1000.0)
|
||||||
|
)
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
player.apply_authoritative_network_input(
|
||||||
|
_movement_input(sequence, sprinting, moving, sneaking)
|
||||||
|
)
|
||||||
|
sequence += 1
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_locomotion_test_position(
|
||||||
|
player: Player,
|
||||||
|
session: NetworkSession,
|
||||||
|
world: TestWorld,
|
||||||
|
) -> void:
|
||||||
|
player.velocity = Vector3.ZERO
|
||||||
|
player.global_transform = world.get_player_spawn_transform()
|
||||||
|
session.publish_authoritative_teleport(1)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await physics_frame
|
||||||
|
|
||||||
|
|
||||||
func _create_initialized_main() -> Node:
|
func _create_initialized_main() -> Node:
|
||||||
root.size = Vector2i(1280, 720)
|
root.size = Vector2i(1280, 720)
|
||||||
var main: Node = MainScene.instantiate()
|
var main: Node = MainScene.instantiate()
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ func _validate_save_migration() -> void:
|
||||||
version_five,
|
version_five,
|
||||||
5,
|
5,
|
||||||
)
|
)
|
||||||
assert(int(migrated.get("save_version", -1)) == 10)
|
assert(int(migrated.get("save_version", -1)) == 11)
|
||||||
var experience_data: Dictionary = migrated.get("experience", {})
|
var experience_data: Dictionary = migrated.get("experience", {})
|
||||||
assert(int(experience_data.get("total_experience", -1)) == 0)
|
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||||
var world_data: Dictionary = migrated.get("world", {})
|
var world_data: Dictionary = migrated.get("world", {})
|
||||||
|
|
@ -155,6 +155,9 @@ func _validate_save_migration() -> void:
|
||||||
assert(
|
assert(
|
||||||
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
|
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
|
||||||
)
|
)
|
||||||
|
assert(not PlayerHomeState.sanitize_save_data(
|
||||||
|
migrated.get("home", {})
|
||||||
|
).is_empty())
|
||||||
manager.queue_free()
|
manager.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,24 @@ func _run() -> void:
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
var data_root := main.get("_data_root") as PlayerDataRoot
|
var data_root := main.get("_data_root") as PlayerDataRoot
|
||||||
assert(save_manager != null and player != null and data_root != null)
|
var home_state := main.get("_home_state") as PlayerHomeState
|
||||||
|
assert(
|
||||||
|
save_manager != null
|
||||||
|
and player != null
|
||||||
|
and data_root != null
|
||||||
|
and home_state != null
|
||||||
|
)
|
||||||
assert(save_manager.initialize_new_game(864209))
|
assert(save_manager.initialize_new_game(864209))
|
||||||
save_manager.set_autosave_enabled(true)
|
save_manager.set_autosave_enabled(true)
|
||||||
assert(player.wallet.restore_balance(4321))
|
assert(player.wallet.restore_balance(4321))
|
||||||
assert(player.bag.add_item(&"coffee"))
|
assert(player.bag.add_item(&"coffee"))
|
||||||
|
assert(home_state.add_owned_decor(&"basic_cube"))
|
||||||
|
assert(not home_state.place_decor(
|
||||||
|
&"basic_cube",
|
||||||
|
Vector3.ZERO,
|
||||||
|
0.0,
|
||||||
|
PlayerHomeState.PlacementMode.GRID,
|
||||||
|
).is_empty())
|
||||||
assert(save_manager.save_now())
|
assert(save_manager.save_now())
|
||||||
|
|
||||||
var save_path: String = str(save_manager.get("_save_path"))
|
var save_path: String = str(save_manager.get("_save_path"))
|
||||||
|
|
@ -38,7 +51,14 @@ func _run() -> void:
|
||||||
_assert_opaque(save_path)
|
_assert_opaque(save_path)
|
||||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
||||||
assert(bool(decoded.get("ok", false)))
|
assert(bool(decoded.get("ok", false)))
|
||||||
assert(int((decoded["data"] as Dictionary)["save_version"]) == 10)
|
assert(int((decoded["data"] as Dictionary)["save_version"]) == 11)
|
||||||
|
assert(not PlayerHomeState.sanitize_save_data(
|
||||||
|
(decoded["data"] as Dictionary).get("home", {})
|
||||||
|
).is_empty())
|
||||||
|
assert(
|
||||||
|
((decoded["data"] as Dictionary)["home"] as Dictionary)
|
||||||
|
.get("placements", []).size() == 1
|
||||||
|
)
|
||||||
var legacy_container_path: String = data_root.progression_backup_directory().path_join(
|
var legacy_container_path: String = data_root.progression_backup_directory().path_join(
|
||||||
"netfishing-container.nfsave"
|
"netfishing-container.nfsave"
|
||||||
)
|
)
|
||||||
|
|
@ -74,6 +94,8 @@ func _run() -> void:
|
||||||
assert(save_manager.load_player_data())
|
assert(save_manager.load_player_data())
|
||||||
assert(player.wallet.get_balance() == 4321)
|
assert(player.wallet.get_balance() == 4321)
|
||||||
assert(save_manager.get_world_seed() == 864209)
|
assert(save_manager.get_world_seed() == 864209)
|
||||||
|
assert(home_state.get_owned_count(&"basic_cube") == 1)
|
||||||
|
assert(home_state.get_placed_count(&"basic_cube") == 1)
|
||||||
|
|
||||||
var legacy_data: Dictionary = (
|
var legacy_data: Dictionary = (
|
||||||
ProgressionSaveCodec.read_local_save(save_path)["data"]
|
ProgressionSaveCodec.read_local_save(save_path)["data"]
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,12 @@ func _run_host() -> void:
|
||||||
assert(bool(main.call(
|
assert(bool(main.call(
|
||||||
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||||
)))
|
)))
|
||||||
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
|
assert(bool(main.call(
|
||||||
assert(session.start_private_host(TEST_PORT))
|
"_prepare_host_world", WorldLayout.STARTER_ISLAND, 1
|
||||||
|
)))
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(save_manager.initialize_new_game())
|
assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
await physics_frame
|
await physics_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
@ -62,6 +64,40 @@ func _run_host() -> void:
|
||||||
remote_peer_id
|
remote_peer_id
|
||||||
)
|
)
|
||||||
assert(remote_record != null)
|
assert(remote_record != null)
|
||||||
|
var network_home := main.get_node(
|
||||||
|
"%NetworkHomeService"
|
||||||
|
) as NetworkHomeService
|
||||||
|
var home_world := main.get_node("%HomeWorldService") as HomeWorldService
|
||||||
|
var spawn_service := main.get_node(
|
||||||
|
"%PlayerSpawnService"
|
||||||
|
) as PlayerSpawnService
|
||||||
|
assert(await _wait_for_home(
|
||||||
|
network_home, remote_record.identity_fingerprint
|
||||||
|
))
|
||||||
|
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||||
|
assert(remote_avatar != null)
|
||||||
|
var exterior_spawn: Transform3D = home_world.get_exterior_spawn_transform(
|
||||||
|
remote_record.identity_fingerprint
|
||||||
|
)
|
||||||
|
var exterior_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < exterior_deadline
|
||||||
|
and remote_avatar.global_position.distance_to(exterior_spawn.origin) > 0.25
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(remote_avatar.global_position.distance_to(exterior_spawn.origin) <= 0.25)
|
||||||
|
var canvas_state: Dictionary = service.get_canvas_state(canvas_id)
|
||||||
|
var canvas_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||||
|
canvas_state["origin"]
|
||||||
|
)
|
||||||
|
remote_avatar.global_position = Vector3(
|
||||||
|
canvas_origin.x + 1.0,
|
||||||
|
player.global_position.y,
|
||||||
|
canvas_origin.z,
|
||||||
|
)
|
||||||
|
session.publish_authoritative_teleport(remote_peer_id)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await physics_frame
|
||||||
assert(session.peer_supports_capability(
|
assert(session.peer_supports_capability(
|
||||||
remote_peer_id, SurfaceDrawingProtocol.CAPABILITY
|
remote_peer_id, SurfaceDrawingProtocol.CAPABILITY
|
||||||
))
|
))
|
||||||
|
|
@ -152,6 +188,9 @@ func _run_host() -> void:
|
||||||
):
|
):
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(service.get_canvas_ids().size() == 1)
|
assert(service.get_canvas_ids().size() == 1)
|
||||||
|
# Keep the second stamp observable on the client before the ban removes all
|
||||||
|
# artwork carrying that participant fingerprint.
|
||||||
|
await create_timer(1.0).timeout
|
||||||
var player_list := main.get_node(
|
var player_list := main.get_node(
|
||||||
"%NetworkPlayerListService"
|
"%NetworkPlayerListService"
|
||||||
) as NetworkPlayerListService
|
) as NetworkPlayerListService
|
||||||
|
|
@ -221,6 +260,20 @@ func _run_client() -> void:
|
||||||
var canvas_id: String = service.get_canvas_ids()[0]
|
var canvas_id: String = service.get_canvas_ids()[0]
|
||||||
var snapshot: Dictionary = service.get_canvas_state(canvas_id)
|
var snapshot: Dictionary = service.get_canvas_state(canvas_id)
|
||||||
assert(str(_find_cell(snapshot, 7, 7).get("color_id", "")) == "coral")
|
assert(str(_find_cell(snapshot, 7, 7).get("color_id", "")) == "coral")
|
||||||
|
var canvas_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||||
|
snapshot["origin"]
|
||||||
|
)
|
||||||
|
var placement_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < placement_deadline
|
||||||
|
and player.global_position.distance_to(canvas_origin)
|
||||||
|
> NetworkSurfaceDrawingService.MAX_DRAW_DISTANCE + 2.0
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(
|
||||||
|
player.global_position.distance_to(canvas_origin)
|
||||||
|
<= NetworkSurfaceDrawingService.MAX_DRAW_DISTANCE + 2.0
|
||||||
|
)
|
||||||
assert(service.request_cell_edits(canvas_id, [{
|
assert(service.request_cell_edits(canvas_id, [{
|
||||||
"x": 7,
|
"x": 7,
|
||||||
"y": 7,
|
"y": 7,
|
||||||
|
|
@ -381,3 +434,18 @@ func _create_initialized_main() -> Node:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
return main
|
return main
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_home(
|
||||||
|
network_home: NetworkHomeService,
|
||||||
|
fingerprint: String,
|
||||||
|
) -> bool:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 10000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
var manifests: Dictionary = network_home.get(
|
||||||
|
"_manifests_by_fingerprint"
|
||||||
|
)
|
||||||
|
if manifests.has(fingerprint):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,13 @@ func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(bool(main.get("_application_initialized")))
|
assert(bool(main.get("_application_initialized")))
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
assert(bool(main.call(
|
||||||
|
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||||
|
)))
|
||||||
|
assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
|
||||||
assert(session.start_private_host(TEST_PORT))
|
assert(session.start_private_host(TEST_PORT))
|
||||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
assert(save_manager.initialize_new_game())
|
assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
|
||||||
main.call("_enter_gameplay")
|
main.call("_enter_gameplay")
|
||||||
await physics_frame
|
await physics_frame
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,41 @@ func _run() -> void:
|
||||||
hotbar.setup(bag, ItemCatalogResource, catches, layout)
|
hotbar.setup(bag, ItemCatalogResource, catches, layout)
|
||||||
assert(PlayerInventoryLayout.INVENTORY_CAPACITIES == [9, 18, 27, 36])
|
assert(PlayerInventoryLayout.INVENTORY_CAPACITIES == [9, 18, 27, 36])
|
||||||
assert(PlayerCoolerCapacity.CAPACITIES == [9, 18, 27, 36, 45, 54, 63, 72])
|
assert(PlayerCoolerCapacity.CAPACITIES == [9, 18, 27, 36, 45, 54, 63, 72])
|
||||||
|
var basic_cube: ItemData = ItemCatalogResource.get_item_by_id(&"basic_cube")
|
||||||
|
assert(basic_cube != null)
|
||||||
|
assert(basic_cube.icon != null)
|
||||||
|
assert(basic_cube.hotbar_allowed)
|
||||||
|
assert(bag.add_item(&"basic_cube"))
|
||||||
|
assert(layout.is_item_in_inventory(&"basic_cube"))
|
||||||
|
assert(hotbar.assign_item(0, &"basic_cube"))
|
||||||
|
assert(hotbar.get_item_id(0) == &"basic_cube")
|
||||||
|
assert(
|
||||||
|
layout.get_container(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM,
|
||||||
|
&"basic_cube",
|
||||||
|
) == PlayerInventoryLayout.InventoryContainer.HOTBAR
|
||||||
|
)
|
||||||
|
assert(hotbar.clear_slot(0))
|
||||||
|
assert(layout.is_item_in_inventory(&"basic_cube"))
|
||||||
|
assert(bag.remove_item(&"basic_cube"))
|
||||||
|
var packed_decor_slot: int = hotbar.find_first_empty_slot()
|
||||||
|
assert(packed_decor_slot == 0)
|
||||||
|
assert(layout.prepare_new_item_placement(
|
||||||
|
&"basic_cube",
|
||||||
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||||
|
packed_decor_slot,
|
||||||
|
))
|
||||||
|
assert(bag.add_item(&"basic_cube"))
|
||||||
|
assert(
|
||||||
|
layout.get_container(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM,
|
||||||
|
&"basic_cube",
|
||||||
|
) == PlayerInventoryLayout.InventoryContainer.HOTBAR
|
||||||
|
)
|
||||||
|
assert(hotbar.assign_item(packed_decor_slot, &"basic_cube"))
|
||||||
|
assert(hotbar.select_slot(packed_decor_slot))
|
||||||
|
assert(hotbar.get_selected_item_id() == &"basic_cube")
|
||||||
|
assert(bag.remove_item(&"basic_cube"))
|
||||||
|
|
||||||
assert(bag.add_item(&"coffee", 3))
|
assert(bag.add_item(&"coffee", 3))
|
||||||
assert(layout.get_inventory_count() == 1)
|
assert(layout.get_inventory_count() == 1)
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,14 @@ func _validate_world_switching() -> void:
|
||||||
assert(world.get_fishing_shop() != null)
|
assert(world.get_fishing_shop() != null)
|
||||||
assert(world.get_player_storage() != null)
|
assert(world.get_player_storage() != null)
|
||||||
assert(not world.get_fishable_water_regions().is_empty())
|
assert(not world.get_fishable_water_regions().is_empty())
|
||||||
|
var starter_safe_points := world.get_safe_respawn_points()
|
||||||
|
assert(not starter_safe_points.is_empty())
|
||||||
|
var authored_safe_point: SafeRespawnPoint = starter_safe_points.back()
|
||||||
|
assert(
|
||||||
|
world.get_water_recovery_position(
|
||||||
|
authored_safe_point.global_position
|
||||||
|
).is_equal_approx(authored_safe_point.global_position)
|
||||||
|
)
|
||||||
assert(world.activate_world(WorldLayout.GENERATED, 24680))
|
assert(world.activate_world(WorldLayout.GENERATED, 24680))
|
||||||
assert(world.get_world_layout() == WorldLayout.GENERATED)
|
assert(world.get_world_layout() == WorldLayout.GENERATED)
|
||||||
assert(world.get_generation_seed() == 24680)
|
assert(world.get_generation_seed() == 24680)
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,18 @@ const CalendarSeasonType = preload("res://world/calendar_season.gd")
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
assert(NetworkProtocol.PROTOCOL_VERSION == 11)
|
call_deferred(&"_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
|
||||||
assert(
|
assert(
|
||||||
NetworkWorldSpawnProtocol.CAPABILITY
|
NetworkWorldSpawnProtocol.CAPABILITY
|
||||||
== NetworkProtocol.WORLD_SPAWN_CAPABILITY
|
== NetworkProtocol.WORLD_SPAWN_CAPABILITY
|
||||||
)
|
)
|
||||||
assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4)
|
assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4)
|
||||||
_validate_catalog_statuses()
|
_validate_catalog_statuses()
|
||||||
_validate_billboard_presentation()
|
await _validate_billboard_presentation()
|
||||||
_validate_envelopes()
|
_validate_envelopes()
|
||||||
print("World spawn protocol validation: PASS")
|
print("World spawn protocol validation: PASS")
|
||||||
quit()
|
quit()
|
||||||
|
|
@ -148,8 +152,27 @@ func _validate_billboard_presentation() -> void:
|
||||||
assert(not sprite.shaded)
|
assert(not sprite.shaded)
|
||||||
gatherable.free()
|
gatherable.free()
|
||||||
|
|
||||||
|
var unconfigured_hotspot := WorldGatherableType.new()
|
||||||
|
unconfigured_hotspot.call("_ensure_water_spurt_visual")
|
||||||
|
assert(
|
||||||
|
unconfigured_hotspot.get_node_or_null(
|
||||||
|
"WaterSpurt/WaterDroplets"
|
||||||
|
) == null
|
||||||
|
)
|
||||||
|
unconfigured_hotspot.free()
|
||||||
|
|
||||||
|
var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
|
||||||
|
assert(clam != null)
|
||||||
var hotspot := WorldGatherableType.new()
|
var hotspot := WorldGatherableType.new()
|
||||||
hotspot.call("_ensure_water_spurt_visual")
|
root.add_child(hotspot)
|
||||||
|
var configured_position := Vector3(24.0, 0.3, -11.0)
|
||||||
|
hotspot.configure(
|
||||||
|
"world:clam-presentation-regression",
|
||||||
|
clam,
|
||||||
|
configured_position,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
assert(hotspot.global_position.is_equal_approx(configured_position))
|
||||||
var hole := hotspot.get_node("WaterSpurt/BurrowMark") as MeshInstance3D
|
var hole := hotspot.get_node("WaterSpurt/BurrowMark") as MeshInstance3D
|
||||||
assert(hole != null)
|
assert(hole != null)
|
||||||
assert(hole.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF)
|
assert(hole.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF)
|
||||||
|
|
@ -158,6 +181,18 @@ func _validate_billboard_presentation() -> void:
|
||||||
assert(hole_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
|
assert(hole_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
|
||||||
assert(hole_material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
|
assert(hole_material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
|
||||||
assert(is_equal_approx(hole_material.albedo_color.a, 1.0))
|
assert(is_equal_approx(hole_material.albedo_color.a, 1.0))
|
||||||
|
var particles := hotspot.get_node(
|
||||||
|
"WaterSpurt/WaterDroplets"
|
||||||
|
) as CPUParticles3D
|
||||||
|
assert(particles != null and particles.emitting)
|
||||||
|
assert(particles.local_coords)
|
||||||
|
assert(particles.global_position.is_equal_approx(
|
||||||
|
configured_position + Vector3.UP * 0.035
|
||||||
|
))
|
||||||
|
hotspot.set_process(false)
|
||||||
|
await create_timer(0.6).timeout
|
||||||
|
await process_frame
|
||||||
|
assert(hotspot.get_node_or_null("WaterSpurt/WaterDroplets") == null)
|
||||||
hotspot.queue_free()
|
hotspot.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
331
ui/decor_shop.gd
Normal file
331
ui/decor_shop.gd
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
class_name DecorShop
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const INPUT_OWNER: StringName = &"decor_shop"
|
||||||
|
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||||
|
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||||
|
|
||||||
|
signal menu_visibility_changed(is_open: bool)
|
||||||
|
signal menu_exit_started
|
||||||
|
|
||||||
|
var _player: Player
|
||||||
|
var _wallet: PlayerWallet
|
||||||
|
var _fishing_spot: FishingSpot
|
||||||
|
var _interaction: DecorShopInteraction
|
||||||
|
var _bag: PlayerBag
|
||||||
|
var _home_state: PlayerHomeState
|
||||||
|
var _network_shop: NetworkShopService
|
||||||
|
var _wallet_label: Label
|
||||||
|
var _owned_label: Label
|
||||||
|
var _feedback: Label
|
||||||
|
var _buy_button: Button
|
||||||
|
var _close_button: Button
|
||||||
|
var _transaction_pending: bool = false
|
||||||
|
var _prior_movement_enabled: bool = true
|
||||||
|
var _prior_camera_enabled: bool = true
|
||||||
|
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
UtilityPageStyleType.apply_page(self)
|
||||||
|
_build_ui()
|
||||||
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
player: Player,
|
||||||
|
wallet: PlayerWallet,
|
||||||
|
fishing_spot: FishingSpot,
|
||||||
|
interaction: DecorShopInteraction,
|
||||||
|
bag: PlayerBag,
|
||||||
|
home_state: PlayerHomeState,
|
||||||
|
network_shop: NetworkShopService,
|
||||||
|
) -> void:
|
||||||
|
_player = player
|
||||||
|
_wallet = wallet
|
||||||
|
_fishing_spot = fishing_spot
|
||||||
|
_interaction = interaction
|
||||||
|
_bag = bag
|
||||||
|
_home_state = home_state
|
||||||
|
_network_shop = network_shop
|
||||||
|
if not _wallet.balance_changed.is_connected(_on_wallet_changed):
|
||||||
|
_wallet.balance_changed.connect(_on_wallet_changed)
|
||||||
|
if not _bag.contents_changed.is_connected(_refresh):
|
||||||
|
_bag.contents_changed.connect(_refresh)
|
||||||
|
if not _home_state.changed.is_connected(_refresh):
|
||||||
|
_home_state.changed.connect(_refresh)
|
||||||
|
if (
|
||||||
|
_network_shop != null
|
||||||
|
and not _network_shop.local_purchase_pending.is_connected(
|
||||||
|
_on_purchase_pending
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_network_shop.local_purchase_pending.connect(_on_purchase_pending)
|
||||||
|
_network_shop.local_purchase_finished.connect(_on_purchase_finished)
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func set_world_interaction(interaction: DecorShopInteraction) -> void:
|
||||||
|
_interaction = interaction
|
||||||
|
|
||||||
|
|
||||||
|
func open_shop() -> bool:
|
||||||
|
if (
|
||||||
|
visible
|
||||||
|
or _player == null
|
||||||
|
or _wallet == null
|
||||||
|
or _fishing_spot == null
|
||||||
|
or _interaction == null
|
||||||
|
or not _interaction.is_local_player_in_range()
|
||||||
|
or not _fishing_spot.can_open_fishing_shop()
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_prior_movement_enabled = _player.is_movement_enabled()
|
||||||
|
_prior_camera_enabled = _player.is_camera_input_enabled()
|
||||||
|
_player.set_movement_enabled(false)
|
||||||
|
# Release any active right-click drag first. The shop must not remember the
|
||||||
|
# drag's transient captured mode because release may happen inside the UI.
|
||||||
|
_player.set_camera_input_enabled(false)
|
||||||
|
_prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode)
|
||||||
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
_transaction_pending = (
|
||||||
|
_network_shop != null
|
||||||
|
and _network_shop.is_local_purchase_pending()
|
||||||
|
)
|
||||||
|
_feedback.text = ""
|
||||||
|
_refresh()
|
||||||
|
show()
|
||||||
|
_buy_button.call_deferred("grab_focus")
|
||||||
|
menu_visibility_changed.emit(true)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func close_shop(restore_controls: bool = true) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
menu_exit_started.emit()
|
||||||
|
get_viewport().gui_release_focus()
|
||||||
|
hide()
|
||||||
|
if _fishing_spot != null:
|
||||||
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
||||||
|
if restore_controls and _player != null:
|
||||||
|
_player.set_movement_enabled(_prior_movement_enabled)
|
||||||
|
_player.set_camera_input_enabled(_prior_camera_enabled)
|
||||||
|
Input.mouse_mode = _prior_mouse_mode
|
||||||
|
menu_visibility_changed.emit(false)
|
||||||
|
|
||||||
|
|
||||||
|
func consume_escape() -> bool:
|
||||||
|
if not visible:
|
||||||
|
return false
|
||||||
|
close_shop()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func close_for_range_exit() -> void:
|
||||||
|
close_shop()
|
||||||
|
|
||||||
|
|
||||||
|
func close_for_water_recovery() -> void:
|
||||||
|
close_shop(false)
|
||||||
|
|
||||||
|
|
||||||
|
func close_for_session_end() -> void:
|
||||||
|
close_shop(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
|
||||||
|
if mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
|
return Input.MOUSE_MODE_VISIBLE
|
||||||
|
return mouse_mode
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not visible or not event.is_action_pressed("ui_cancel"):
|
||||||
|
return
|
||||||
|
close_shop()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _purchase_cube() -> void:
|
||||||
|
if _network_shop == null or _transaction_pending:
|
||||||
|
return
|
||||||
|
var request_id: String = _network_shop.request_decor(
|
||||||
|
DecorCatalogType.BASIC_CUBE_ID
|
||||||
|
)
|
||||||
|
if request_id.is_empty():
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_purchase_pending(_request_id: String) -> void:
|
||||||
|
_transaction_pending = true
|
||||||
|
_feedback.text = "purchasing…"
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_purchase_finished(
|
||||||
|
_request_id: String,
|
||||||
|
accepted: bool,
|
||||||
|
message: String,
|
||||||
|
product_id: StringName,
|
||||||
|
category: int,
|
||||||
|
_quantity: int,
|
||||||
|
total_cost: int,
|
||||||
|
) -> void:
|
||||||
|
if (
|
||||||
|
category != NetworkShopProtocol.ProductCategory.DECOR
|
||||||
|
or product_id != DecorCatalogType.BASIC_CUBE_ID
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_transaction_pending = false
|
||||||
|
_feedback.text = (
|
||||||
|
"basic cube purchased · %d fishcoin spent" % total_cost
|
||||||
|
if accepted
|
||||||
|
else message.to_lower()
|
||||||
|
)
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_wallet_changed(_balance: int, _delta: int) -> void:
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh() -> void:
|
||||||
|
if _wallet_label == null:
|
||||||
|
return
|
||||||
|
var balance: int = _wallet.get_balance() if _wallet != null else 0
|
||||||
|
var owned: int = (
|
||||||
|
_home_state.get_owned_count(DecorCatalogType.BASIC_CUBE_ID)
|
||||||
|
if _home_state != null else 0
|
||||||
|
)
|
||||||
|
var carried: int = (
|
||||||
|
_bag.get_quantity(DecorCatalogType.BASIC_CUBE_ID)
|
||||||
|
if _bag != null else 0
|
||||||
|
)
|
||||||
|
_wallet_label.text = "%d fishcoins" % balance
|
||||||
|
_owned_label.text = "owned %d · packed %d" % [owned, carried]
|
||||||
|
_buy_button.disabled = (
|
||||||
|
_transaction_pending
|
||||||
|
or _wallet == null
|
||||||
|
or not _wallet.can_afford(
|
||||||
|
DecorCatalogType.get_price(DecorCatalogType.BASIC_CUBE_ID)
|
||||||
|
)
|
||||||
|
or _bag == null
|
||||||
|
or not _bag.can_add_item(DecorCatalogType.BASIC_CUBE_ID, 1)
|
||||||
|
or owned >= DecorCatalogType.MAX_OWNED_PER_PRODUCT
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
z_index = 83
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
var blocker := ColorRect.new()
|
||||||
|
blocker.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
blocker.color = Color(0.02, 0.08, 0.10, 0.55)
|
||||||
|
blocker.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
add_child(blocker)
|
||||||
|
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
panel.position = Vector2(-440.0, -250.0)
|
||||||
|
panel.size = Vector2(880.0, 500.0)
|
||||||
|
panel.add_theme_stylebox_override(
|
||||||
|
"panel",
|
||||||
|
UtilityPageStyleType.rounded_style(
|
||||||
|
UtilityPageStyleType.OCEAN_PANEL_DEEP, 24
|
||||||
|
),
|
||||||
|
)
|
||||||
|
add_child(panel)
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
for side: StringName in [
|
||||||
|
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom",
|
||||||
|
]:
|
||||||
|
margin.add_theme_constant_override(side, 28)
|
||||||
|
panel.add_child(margin)
|
||||||
|
var layout := VBoxContainer.new()
|
||||||
|
layout.add_theme_constant_override("separation", 18)
|
||||||
|
margin.add_child(layout)
|
||||||
|
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
header.add_theme_constant_override("separation", 12)
|
||||||
|
layout.add_child(header)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "decor"
|
||||||
|
title.add_theme_font_size_override("font_size", 38)
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
header.add_child(title)
|
||||||
|
_wallet_label = Label.new()
|
||||||
|
_wallet_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
_wallet_label.add_theme_font_size_override("font_size", 22)
|
||||||
|
header.add_child(_wallet_label)
|
||||||
|
|
||||||
|
var item_panel := PanelContainer.new()
|
||||||
|
item_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
item_panel.add_theme_stylebox_override(
|
||||||
|
"panel",
|
||||||
|
UtilityPageStyleType.rounded_style(
|
||||||
|
UtilityPageStyleType.OCEAN_FIELD, 18
|
||||||
|
),
|
||||||
|
)
|
||||||
|
layout.add_child(item_panel)
|
||||||
|
var item_margin := MarginContainer.new()
|
||||||
|
for side: StringName in [
|
||||||
|
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom",
|
||||||
|
]:
|
||||||
|
item_margin.add_theme_constant_override(side, 24)
|
||||||
|
item_panel.add_child(item_margin)
|
||||||
|
var item_row := HBoxContainer.new()
|
||||||
|
item_row.add_theme_constant_override("separation", 28)
|
||||||
|
item_margin.add_child(item_row)
|
||||||
|
var preview := Panel.new()
|
||||||
|
preview.custom_minimum_size = Vector2(150.0, 150.0)
|
||||||
|
preview.add_theme_stylebox_override(
|
||||||
|
"panel",
|
||||||
|
UtilityPageStyleType.rounded_style(Color("b77a55"), 12),
|
||||||
|
)
|
||||||
|
item_row.add_child(preview)
|
||||||
|
var details := VBoxContainer.new()
|
||||||
|
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
details.add_theme_constant_override("separation", 10)
|
||||||
|
item_row.add_child(details)
|
||||||
|
var item_name := Label.new()
|
||||||
|
item_name.text = DecorCatalogType.get_display_name(
|
||||||
|
DecorCatalogType.BASIC_CUBE_ID
|
||||||
|
)
|
||||||
|
item_name.add_theme_font_size_override("font_size", 32)
|
||||||
|
details.add_child(item_name)
|
||||||
|
var description := Label.new()
|
||||||
|
description.text = DecorCatalogType.get_description(
|
||||||
|
DecorCatalogType.BASIC_CUBE_ID
|
||||||
|
)
|
||||||
|
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
description.add_theme_font_size_override("font_size", 20)
|
||||||
|
description.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
details.add_child(description)
|
||||||
|
_owned_label = Label.new()
|
||||||
|
_owned_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
details.add_child(_owned_label)
|
||||||
|
_buy_button = Button.new()
|
||||||
|
_buy_button.text = "buy · 1 fishcoin"
|
||||||
|
_buy_button.custom_minimum_size = Vector2(260.0, 58.0)
|
||||||
|
UtilityPageStyleType.apply_ocean_button(_buy_button)
|
||||||
|
_buy_button.pressed.connect(_purchase_cube)
|
||||||
|
details.add_child(_buy_button)
|
||||||
|
|
||||||
|
var footer := HBoxContainer.new()
|
||||||
|
footer.add_theme_constant_override("separation", 12)
|
||||||
|
layout.add_child(footer)
|
||||||
|
_feedback = Label.new()
|
||||||
|
_feedback.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
_feedback.add_theme_font_size_override("font_size", 18)
|
||||||
|
footer.add_child(_feedback)
|
||||||
|
_close_button = Button.new()
|
||||||
|
_close_button.text = "close"
|
||||||
|
_close_button.custom_minimum_size = Vector2(150.0, 50.0)
|
||||||
|
UtilityPageStyleType.apply_ocean_button(_close_button)
|
||||||
|
_close_button.pressed.connect(close_shop)
|
||||||
|
footer.add_child(_close_button)
|
||||||
|
_buy_button.focus_neighbor_bottom = _buy_button.get_path_to(_close_button)
|
||||||
|
_close_button.focus_neighbor_top = _close_button.get_path_to(_buy_button)
|
||||||
1
ui/decor_shop.gd.uid
Normal file
1
ui/decor_shop.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://810ck3mvbn5v
|
||||||
14
ui/decor_shop.tscn
Normal file
14
ui/decor_shop.tscn
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/decor_shop.gd" id="1_script"]
|
||||||
|
|
||||||
|
[node name="DecorShop" type="Control"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
@ -654,11 +654,14 @@ func open_shop() -> bool:
|
||||||
)
|
)
|
||||||
_prior_movement_enabled = _player.is_movement_enabled()
|
_prior_movement_enabled = _player.is_movement_enabled()
|
||||||
_prior_camera_enabled = _player.is_camera_input_enabled()
|
_prior_camera_enabled = _player.is_camera_input_enabled()
|
||||||
_prior_mouse_mode = Input.mouse_mode
|
_player.set_movement_enabled(false)
|
||||||
|
# Releasing an active camera drag before the snapshot prevents the close
|
||||||
|
# path from restoring a captured pointer after its matching button release
|
||||||
|
# has been consumed by the shop.
|
||||||
|
_player.set_camera_input_enabled(false)
|
||||||
|
_prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode)
|
||||||
_snapshot_stored = true
|
_snapshot_stored = true
|
||||||
_mouse_snapshot_stored = true
|
_mouse_snapshot_stored = true
|
||||||
_player.set_movement_enabled(false)
|
|
||||||
_player.set_camera_input_enabled(false)
|
|
||||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
_set_feedback("")
|
_set_feedback("")
|
||||||
|
|
@ -1897,3 +1900,9 @@ func _apply_mouse_close_policy(reason: CloseReason) -> void:
|
||||||
CloseReason.TEARDOWN:
|
CloseReason.TEARDOWN:
|
||||||
pass
|
pass
|
||||||
_mouse_snapshot_stored = false
|
_mouse_snapshot_stored = false
|
||||||
|
|
||||||
|
|
||||||
|
func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
|
||||||
|
if mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
|
return Input.MOUSE_MODE_VISIBLE
|
||||||
|
return mouse_mode
|
||||||
|
|
|
||||||
105
ui/game_ui.gd
105
ui/game_ui.gd
|
|
@ -20,6 +20,7 @@ const TitleScreenType = preload("res://ui/title_screen.gd")
|
||||||
const PauseMenuType = preload("res://ui/pause_menu.gd")
|
const PauseMenuType = preload("res://ui/pause_menu.gd")
|
||||||
const NetworkSessionType = preload("res://network/network_session.gd")
|
const NetworkSessionType = preload("res://network/network_session.gd")
|
||||||
const FishingShopType = preload("res://ui/fishing_shop.gd")
|
const FishingShopType = preload("res://ui/fishing_shop.gd")
|
||||||
|
const DecorShopType = preload("res://ui/decor_shop.gd")
|
||||||
const SettingsPanelType = preload("res://ui/settings_panel.gd")
|
const SettingsPanelType = preload("res://ui/settings_panel.gd")
|
||||||
const PlayerFishingUpgradesType = preload(
|
const PlayerFishingUpgradesType = preload(
|
||||||
"res://progression/player_fishing_upgrades.gd"
|
"res://progression/player_fishing_upgrades.gd"
|
||||||
|
|
@ -27,7 +28,11 @@ const PlayerFishingUpgradesType = preload(
|
||||||
const ShopInteractionType = preload(
|
const ShopInteractionType = preload(
|
||||||
"res://world/fishing_shop_interaction.gd"
|
"res://world/fishing_shop_interaction.gd"
|
||||||
)
|
)
|
||||||
|
const DecorShopInteractionType = preload(
|
||||||
|
"res://world/decor_shop_interaction.gd"
|
||||||
|
)
|
||||||
const PlayerStorageType = preload("res://ui/player_storage.gd")
|
const PlayerStorageType = preload("res://ui/player_storage.gd")
|
||||||
|
const HomeDecorToolbarType = preload("res://ui/home_decor_toolbar.gd")
|
||||||
const PlayerStorageInteractionType = preload(
|
const PlayerStorageInteractionType = preload(
|
||||||
"res://world/player_storage_interaction.gd"
|
"res://world/player_storage_interaction.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -71,6 +76,9 @@ const MAIN_BUBBLE_PROFILE: BubbleMenuProfile = preload(
|
||||||
const ACTIVE_BAIT_EMPTY_ICON: Texture2D = preload(
|
const ACTIVE_BAIT_EMPTY_ICON: Texture2D = preload(
|
||||||
"res://ui/icons/pictograms/x_dark.png"
|
"res://ui/icons/pictograms/x_dark.png"
|
||||||
)
|
)
|
||||||
|
const RV_STORAGE_ICON: Texture2D = preload(
|
||||||
|
"res://ui/icons/tab_menu/inventory_simple.png"
|
||||||
|
)
|
||||||
|
|
||||||
signal pixelation_settings_visibility_changed(is_visible: bool)
|
signal pixelation_settings_visibility_changed(is_visible: bool)
|
||||||
signal crisp_reset_focus_requested
|
signal crisp_reset_focus_requested
|
||||||
|
|
@ -82,6 +90,7 @@ signal virtual_pointer_mode_changed(is_active: bool)
|
||||||
signal social_prompt_accepted
|
signal social_prompt_accepted
|
||||||
signal social_prompt_declined
|
signal social_prompt_declined
|
||||||
signal data_root_changed
|
signal data_root_changed
|
||||||
|
signal rv_storage_requested
|
||||||
|
|
||||||
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse"
|
||||||
const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu"
|
const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu"
|
||||||
|
|
@ -145,7 +154,10 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
|
||||||
@onready var _social_prompt: BubbleConfirmationPage = %SocialPrompt
|
@onready var _social_prompt: BubbleConfirmationPage = %SocialPrompt
|
||||||
@onready var _hotbar_ui: HotbarUIType = %Hotbar
|
@onready var _hotbar_ui: HotbarUIType = %Hotbar
|
||||||
@onready var _fishing_shop: FishingShopType = %FishingShop
|
@onready var _fishing_shop: FishingShopType = %FishingShop
|
||||||
|
@onready var _decor_shop: DecorShopType = %DecorShop
|
||||||
@onready var _player_storage: PlayerStorageType = %PlayerStorage
|
@onready var _player_storage: PlayerStorageType = %PlayerStorage
|
||||||
|
@onready var _home_decor_toolbar: HomeDecorToolbarType = %HomeDecorToolbar
|
||||||
|
@onready var _rv_storage_button: Button = %RVStorageButton
|
||||||
@onready var _storage_prompt: PanelContainer = %StoragePrompt
|
@onready var _storage_prompt: PanelContainer = %StoragePrompt
|
||||||
@onready var _storage_prompt_message: Label = %StoragePromptMessage
|
@onready var _storage_prompt_message: Label = %StoragePromptMessage
|
||||||
@onready var _shop_prompt: Control = %ShopPrompt
|
@onready var _shop_prompt: Control = %ShopPrompt
|
||||||
|
|
@ -195,6 +207,7 @@ var _chat_input_open: bool = false
|
||||||
var _player_menu_hotbar_visible: bool = false
|
var _player_menu_hotbar_visible: bool = false
|
||||||
var _main_shop_buyer: FishBuyerProfileType
|
var _main_shop_buyer: FishBuyerProfileType
|
||||||
var _shop_interaction: ShopInteractionType
|
var _shop_interaction: ShopInteractionType
|
||||||
|
var _decor_shop_interaction: DecorShopInteractionType
|
||||||
var _storage_interaction: PlayerStorageInteractionType
|
var _storage_interaction: PlayerStorageInteractionType
|
||||||
var _surface_drawing: NetworkSurfaceDrawingService
|
var _surface_drawing: NetworkSurfaceDrawingService
|
||||||
var _surface_drawing_hotbar_selected: bool = false
|
var _surface_drawing_hotbar_selected: bool = false
|
||||||
|
|
@ -221,14 +234,17 @@ var _controller_text_entry_request: Callable
|
||||||
var _controller_text_entry_is_open: Callable
|
var _controller_text_entry_is_open: Callable
|
||||||
var _controller_text_entry_close: Callable
|
var _controller_text_entry_close: Callable
|
||||||
var _restore_chat_keyboard_after_fishing: bool = false
|
var _restore_chat_keyboard_after_fishing: bool = false
|
||||||
|
var _rv_storage_button_requested_visible: bool = false
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_prioritize_surface_drawing_pointer_input()
|
_prioritize_surface_drawing_pointer_input()
|
||||||
_bite_prompt_button.pressed.connect(_on_bite_prompt_pressed)
|
_bite_prompt_button.pressed.connect(_on_bite_prompt_pressed)
|
||||||
|
_rv_storage_button.pressed.connect(_on_rv_storage_button_pressed)
|
||||||
_social_prompt.confirmed.connect(_accept_social_prompt)
|
_social_prompt.confirmed.connect(_accept_social_prompt)
|
||||||
_social_prompt.cancelled.connect(_decline_social_prompt)
|
_social_prompt.cancelled.connect(_decline_social_prompt)
|
||||||
_apply_active_bait_indicator_style()
|
_apply_active_bait_indicator_style()
|
||||||
|
_apply_rv_storage_button_style()
|
||||||
_refresh_active_bait_indicator()
|
_refresh_active_bait_indicator()
|
||||||
_refresh_interaction_prompt_bindings()
|
_refresh_interaction_prompt_bindings()
|
||||||
# Reward feedback must remain above full-screen canonical menus. Keeping the
|
# Reward feedback must remain above full-screen canonical menus. Keeping the
|
||||||
|
|
@ -260,6 +276,9 @@ func _ready() -> void:
|
||||||
_player_menu.controller_hotbar_management_ended.connect(
|
_player_menu.controller_hotbar_management_ended.connect(
|
||||||
_on_controller_hotbar_management_ended
|
_on_controller_hotbar_management_ended
|
||||||
)
|
)
|
||||||
|
_player_menu.controller_text_entry_requested.connect(
|
||||||
|
request_controller_text_entry_for
|
||||||
|
)
|
||||||
_title_settings_panel.panel_visibility_changed.connect(
|
_title_settings_panel.panel_visibility_changed.connect(
|
||||||
_on_settings_visibility_changed
|
_on_settings_visibility_changed
|
||||||
)
|
)
|
||||||
|
|
@ -343,11 +362,13 @@ func setup(
|
||||||
fishing_upgrades: PlayerFishingUpgradesType,
|
fishing_upgrades: PlayerFishingUpgradesType,
|
||||||
shop_interaction: ShopInteractionType,
|
shop_interaction: ShopInteractionType,
|
||||||
storage_interaction: PlayerStorageInteractionType,
|
storage_interaction: PlayerStorageInteractionType,
|
||||||
|
decor_shop_interaction: DecorShopInteractionType,
|
||||||
item_effects: PlayerItemEffectsType,
|
item_effects: PlayerItemEffectsType,
|
||||||
cooler_capacity: PlayerCoolerCapacityType,
|
cooler_capacity: PlayerCoolerCapacityType,
|
||||||
network_session: NetworkSessionType,
|
network_session: NetworkSessionType,
|
||||||
network_sale_service: NetworkSaleService,
|
network_sale_service: NetworkSaleService,
|
||||||
network_shop_service: NetworkShopService,
|
network_shop_service: NetworkShopService,
|
||||||
|
home_state: PlayerHomeState,
|
||||||
network_chat_service: NetworkChatService,
|
network_chat_service: NetworkChatService,
|
||||||
network_profile: NetworkProfilePreferences,
|
network_profile: NetworkProfilePreferences,
|
||||||
spawn_service: PlayerSpawnService,
|
spawn_service: PlayerSpawnService,
|
||||||
|
|
@ -459,6 +480,7 @@ func setup(
|
||||||
discovery,
|
discovery,
|
||||||
player_jobs,
|
player_jobs,
|
||||||
world_time,
|
world_time,
|
||||||
|
world_weather,
|
||||||
world_environment,
|
world_environment,
|
||||||
world_sun,
|
world_sun,
|
||||||
)
|
)
|
||||||
|
|
@ -490,6 +512,15 @@ func setup(
|
||||||
network_sale_service,
|
network_sale_service,
|
||||||
reservations,
|
reservations,
|
||||||
)
|
)
|
||||||
|
_decor_shop.setup(
|
||||||
|
player,
|
||||||
|
wallet,
|
||||||
|
fishing_spot,
|
||||||
|
decor_shop_interaction,
|
||||||
|
bag,
|
||||||
|
home_state,
|
||||||
|
network_shop_service,
|
||||||
|
)
|
||||||
_player_storage.setup(
|
_player_storage.setup(
|
||||||
player,
|
player,
|
||||||
fishing_spot,
|
fishing_spot,
|
||||||
|
|
@ -505,6 +536,8 @@ func setup(
|
||||||
)
|
)
|
||||||
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
|
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
|
||||||
_fishing_shop.menu_exit_started.connect(_on_shop_exit_started)
|
_fishing_shop.menu_exit_started.connect(_on_shop_exit_started)
|
||||||
|
_decor_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
|
||||||
|
_decor_shop.menu_exit_started.connect(_on_shop_exit_started)
|
||||||
_fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested)
|
_fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested)
|
||||||
_fishing_shop.shop_cooler_return_requested.connect(
|
_fishing_shop.shop_cooler_return_requested.connect(
|
||||||
_on_shop_cooler_return_requested
|
_on_shop_cooler_return_requested
|
||||||
|
|
@ -514,6 +547,7 @@ func setup(
|
||||||
)
|
)
|
||||||
_main_shop_buyer = main_shop_buyer
|
_main_shop_buyer = main_shop_buyer
|
||||||
_shop_interaction = shop_interaction
|
_shop_interaction = shop_interaction
|
||||||
|
_decor_shop_interaction = decor_shop_interaction
|
||||||
_storage_interaction = storage_interaction
|
_storage_interaction = storage_interaction
|
||||||
_surface_drawing = surface_drawing
|
_surface_drawing = surface_drawing
|
||||||
if (
|
if (
|
||||||
|
|
@ -536,11 +570,14 @@ func setup(
|
||||||
func set_world_interactions(
|
func set_world_interactions(
|
||||||
shop_interaction: ShopInteractionType,
|
shop_interaction: ShopInteractionType,
|
||||||
storage_interaction: PlayerStorageInteractionType,
|
storage_interaction: PlayerStorageInteractionType,
|
||||||
|
decor_shop_interaction: DecorShopInteractionType,
|
||||||
) -> void:
|
) -> void:
|
||||||
_shop_interaction = shop_interaction
|
_shop_interaction = shop_interaction
|
||||||
_storage_interaction = storage_interaction
|
_storage_interaction = storage_interaction
|
||||||
|
_decor_shop_interaction = decor_shop_interaction
|
||||||
_fishing_shop.set_world_interaction(shop_interaction)
|
_fishing_shop.set_world_interaction(shop_interaction)
|
||||||
_player_storage.set_world_interaction(storage_interaction)
|
_player_storage.set_world_interaction(storage_interaction)
|
||||||
|
_decor_shop.set_world_interaction(decor_shop_interaction)
|
||||||
|
|
||||||
|
|
||||||
func _prioritize_surface_drawing_pointer_input() -> void:
|
func _prioritize_surface_drawing_pointer_input() -> void:
|
||||||
|
|
@ -1651,6 +1688,34 @@ func _apply_active_bait_indicator_style() -> void:
|
||||||
_active_bait_button.add_theme_color_override(state, Color.WHITE)
|
_active_bait_button.add_theme_color_override(state, Color.WHITE)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_rv_storage_button_style() -> void:
|
||||||
|
var normal_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_normal_style()
|
||||||
|
var hover_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_hover_style()
|
||||||
|
var pressed_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_pressed_style()
|
||||||
|
for style: StyleBoxFlat in [normal_style, hover_style, pressed_style]:
|
||||||
|
style.set_corner_radius_all(29)
|
||||||
|
style.content_margin_left = 9.0
|
||||||
|
style.content_margin_top = 9.0
|
||||||
|
style.content_margin_right = 9.0
|
||||||
|
style.content_margin_bottom = 9.0
|
||||||
|
_rv_storage_button.icon = RV_STORAGE_ICON
|
||||||
|
_rv_storage_button.add_theme_stylebox_override("normal", normal_style)
|
||||||
|
_rv_storage_button.add_theme_stylebox_override("hover", hover_style)
|
||||||
|
_rv_storage_button.add_theme_stylebox_override("focus", hover_style)
|
||||||
|
_rv_storage_button.add_theme_stylebox_override("pressed", pressed_style)
|
||||||
|
for state: StringName in [
|
||||||
|
&"icon_normal_color",
|
||||||
|
&"icon_hover_color",
|
||||||
|
&"icon_focus_color",
|
||||||
|
&"icon_pressed_color",
|
||||||
|
]:
|
||||||
|
_rv_storage_button.add_theme_color_override(state, Color.WHITE)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_rv_storage_button_pressed() -> void:
|
||||||
|
rv_storage_requested.emit()
|
||||||
|
|
||||||
|
|
||||||
func _refresh_active_bait_indicator() -> void:
|
func _refresh_active_bait_indicator() -> void:
|
||||||
_refresh_active_bait_indicator_visibility()
|
_refresh_active_bait_indicator_visibility()
|
||||||
if not is_node_ready():
|
if not is_node_ready():
|
||||||
|
|
@ -1714,17 +1779,20 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||||
if not enabled:
|
if not enabled:
|
||||||
_gameplay_hud_hidden = false
|
_gameplay_hud_hidden = false
|
||||||
_refresh_active_bait_indicator_visibility()
|
_refresh_active_bait_indicator_visibility()
|
||||||
|
_refresh_rv_storage_button_visibility()
|
||||||
if enabled:
|
if enabled:
|
||||||
_try_start_shop_npc_speech()
|
_try_start_shop_npc_speech()
|
||||||
_refresh_gameplay_hud_visibility()
|
_refresh_gameplay_hud_visibility()
|
||||||
_refresh_chat_availability()
|
_refresh_chat_availability()
|
||||||
if not enabled:
|
if not enabled:
|
||||||
|
_home_decor_toolbar.set_active(false)
|
||||||
_end_virtual_mouse()
|
_end_virtual_mouse()
|
||||||
_close_radial_menus()
|
_close_radial_menus()
|
||||||
if _surface_drawing != null:
|
if _surface_drawing != null:
|
||||||
_surface_drawing.deactivate()
|
_surface_drawing.deactivate()
|
||||||
close_player_menu_for_session_end()
|
close_player_menu_for_session_end()
|
||||||
_fishing_shop.close_for_session_end()
|
_fishing_shop.close_for_session_end()
|
||||||
|
_decor_shop.close_for_session_end()
|
||||||
_player_storage.close_for_session_end()
|
_player_storage.close_for_session_end()
|
||||||
_fishing_panel.visible = false
|
_fishing_panel.visible = false
|
||||||
_shop_prompt.hide()
|
_shop_prompt.hide()
|
||||||
|
|
@ -1745,6 +1813,7 @@ func set_gameplay_hud_hidden(hidden: bool) -> void:
|
||||||
_quick_radial_menu.set_hud_hidden(hidden)
|
_quick_radial_menu.set_hud_hidden(hidden)
|
||||||
_refresh_gameplay_hud_visibility()
|
_refresh_gameplay_hud_visibility()
|
||||||
_refresh_active_bait_indicator_visibility()
|
_refresh_active_bait_indicator_visibility()
|
||||||
|
_refresh_rv_storage_button_visibility()
|
||||||
_refresh_hotbar_visibility()
|
_refresh_hotbar_visibility()
|
||||||
_refresh_chat_availability()
|
_refresh_chat_availability()
|
||||||
|
|
||||||
|
|
@ -1860,13 +1929,43 @@ func get_fishing_shop() -> FishingShopType:
|
||||||
return _fishing_shop
|
return _fishing_shop
|
||||||
|
|
||||||
|
|
||||||
|
func get_decor_shop() -> DecorShopType:
|
||||||
|
return _decor_shop
|
||||||
|
|
||||||
|
|
||||||
func get_player_storage() -> PlayerStorageType:
|
func get_player_storage() -> PlayerStorageType:
|
||||||
return _player_storage
|
return _player_storage
|
||||||
|
|
||||||
|
|
||||||
|
func get_home_decor_toolbar() -> HomeDecorToolbarType:
|
||||||
|
return _home_decor_toolbar
|
||||||
|
|
||||||
|
|
||||||
|
func set_rv_interior_camera_mode(enabled: bool) -> void:
|
||||||
|
_hotbar_ui.set_rv_interior_camera_mode(enabled)
|
||||||
|
|
||||||
|
|
||||||
|
func set_rv_storage_button_visible(requested_visible: bool) -> void:
|
||||||
|
_rv_storage_button_requested_visible = requested_visible
|
||||||
|
_refresh_rv_storage_button_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_rv_storage_button_visibility() -> void:
|
||||||
|
_rv_storage_button.visible = (
|
||||||
|
_rv_storage_button_requested_visible
|
||||||
|
and _gameplay_ui_enabled
|
||||||
|
and not _gameplay_hud_hidden
|
||||||
|
and not _system_menu_open
|
||||||
|
and not _player_menu_open
|
||||||
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func set_storage_prompt_visible(
|
func set_storage_prompt_visible(
|
||||||
requested_visible: bool,
|
requested_visible: bool,
|
||||||
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
||||||
|
action_text: String = "open storage",
|
||||||
) -> void:
|
) -> void:
|
||||||
if not _storage_prompt.has_meta(&"styled"):
|
if not _storage_prompt.has_meta(&"styled"):
|
||||||
_storage_prompt.set_meta(&"styled", true)
|
_storage_prompt.set_meta(&"styled", true)
|
||||||
|
|
@ -1875,6 +1974,7 @@ func set_storage_prompt_visible(
|
||||||
)
|
)
|
||||||
style.anti_aliasing = false
|
style.anti_aliasing = false
|
||||||
_storage_prompt.add_theme_stylebox_override("panel", style)
|
_storage_prompt.add_theme_stylebox_override("panel", style)
|
||||||
|
_storage_prompt_message.text = action_text
|
||||||
_storage_prompt.visible = (
|
_storage_prompt.visible = (
|
||||||
requested_visible
|
requested_visible
|
||||||
and _gameplay_ui_enabled
|
and _gameplay_ui_enabled
|
||||||
|
|
@ -1892,7 +1992,7 @@ func _position_storage_prompt(world_anchor: Vector3) -> void:
|
||||||
if _player == null:
|
if _player == null:
|
||||||
_storage_prompt.hide()
|
_storage_prompt.hide()
|
||||||
return
|
return
|
||||||
var camera := _player.get_gameplay_camera()
|
var camera := _player.get_active_gameplay_camera()
|
||||||
if camera == null or camera.is_position_behind(world_anchor):
|
if camera == null or camera.is_position_behind(world_anchor):
|
||||||
_storage_prompt.hide()
|
_storage_prompt.hide()
|
||||||
return
|
return
|
||||||
|
|
@ -2006,7 +2106,7 @@ func _position_shop_prompt(world_anchor: Vector3) -> void:
|
||||||
if _player == null:
|
if _player == null:
|
||||||
_shop_prompt.hide()
|
_shop_prompt.hide()
|
||||||
return
|
return
|
||||||
var camera: Camera3D = _player.get_gameplay_camera()
|
var camera: Camera3D = _player.get_active_gameplay_camera()
|
||||||
if camera == null or camera.is_position_behind(world_anchor):
|
if camera == null or camera.is_position_behind(world_anchor):
|
||||||
_shop_prompt.hide()
|
_shop_prompt.hide()
|
||||||
return
|
return
|
||||||
|
|
@ -2758,6 +2858,7 @@ func _refresh_hotbar_visibility() -> void:
|
||||||
|
|
||||||
func _emit_interactive_pointer_ui_changed() -> void:
|
func _emit_interactive_pointer_ui_changed() -> void:
|
||||||
_refresh_active_bait_indicator_visibility()
|
_refresh_active_bait_indicator_visibility()
|
||||||
|
_refresh_rv_storage_button_visibility()
|
||||||
interactive_pointer_ui_changed.emit(
|
interactive_pointer_ui_changed.emit(
|
||||||
_system_menu_open
|
_system_menu_open
|
||||||
or _player_menu_open
|
or _player_menu_open
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=22 format=3]
|
[gd_scene load_steps=24 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
[ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"]
|
[ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/player_storage.tscn" id="14_storage"]
|
[ext_resource type="PackedScene" path="res://ui/player_storage.tscn" id="14_storage"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="15_social_prompt"]
|
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="15_social_prompt"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/decor_shop.tscn" id="16_decor_shop"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/home_decor_toolbar.tscn" id="17_decor_toolbar"]
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
|
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
|
||||||
bg_color = Color(0.032, 0.118, 0.15, 1)
|
bg_color = Color(0.032, 0.118, 0.15, 1)
|
||||||
|
|
@ -472,9 +474,37 @@ polygon = PackedVector2Array(-8, 0, 8, 0, 0, 10)
|
||||||
[node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")]
|
[node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
[node name="DecorShop" parent="UIRoot/CanonicalStage" instance=ExtResource("16_decor_shop")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
[node name="HomeDecorToolbar" parent="UIRoot/CanonicalStage" instance=ExtResource("17_decor_toolbar")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
|
||||||
[node name="PlayerStorage" parent="UIRoot/CanonicalStage" instance=ExtResource("14_storage")]
|
[node name="PlayerStorage" parent="UIRoot/CanonicalStage" instance=ExtResource("14_storage")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
[node name="RVStorageButton" type="Button" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
z_index = 57
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 1
|
||||||
|
anchor_left = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
offset_left = -74.0
|
||||||
|
offset_top = 16.0
|
||||||
|
offset_right = -16.0
|
||||||
|
offset_bottom = 74.0
|
||||||
|
grow_horizontal = 0
|
||||||
|
mouse_default_cursor_shape = 2
|
||||||
|
focus_mode = 0
|
||||||
|
tooltip_text = "open RV storage"
|
||||||
|
theme = ExtResource("3_theme")
|
||||||
|
texture_filter = 1
|
||||||
|
expand_icon = true
|
||||||
|
icon_alignment = 1
|
||||||
|
vertical_icon_alignment = 1
|
||||||
|
|
||||||
[node name="StoragePrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
[node name="StoragePrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
|
|
|
||||||
126
ui/home_decor_toolbar.gd
Normal file
126
ui/home_decor_toolbar.gd
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
class_name HomeDecorToolbar
|
||||||
|
extends PanelContainer
|
||||||
|
|
||||||
|
signal placement_mode_requested(mode: int)
|
||||||
|
signal rotate_requested
|
||||||
|
signal store_requested
|
||||||
|
signal cancel_requested
|
||||||
|
|
||||||
|
var _mode_button: Button
|
||||||
|
var _selection_label: Label
|
||||||
|
var _rotate_button: Button
|
||||||
|
var _store_button: Button
|
||||||
|
var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build_ui()
|
||||||
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
func set_active(active: bool) -> void:
|
||||||
|
if visible == active:
|
||||||
|
return
|
||||||
|
visible = active
|
||||||
|
if active and _mode_button != null:
|
||||||
|
_mode_button.call_deferred("grab_focus")
|
||||||
|
|
||||||
|
|
||||||
|
func set_mode(mode: PlayerHomeState.PlacementMode) -> void:
|
||||||
|
_mode = mode
|
||||||
|
if _mode_button != null:
|
||||||
|
_mode_button.text = (
|
||||||
|
"placement: grid"
|
||||||
|
if mode == PlayerHomeState.PlacementMode.GRID
|
||||||
|
else "placement: free"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func set_selection(instance_id: String, display_name: String = "") -> void:
|
||||||
|
var selected: bool = not instance_id.is_empty()
|
||||||
|
if _selection_label != null:
|
||||||
|
_selection_label.text = (
|
||||||
|
"selected: %s" % display_name
|
||||||
|
if selected else "click decor to select; drag to move"
|
||||||
|
)
|
||||||
|
if _rotate_button != null:
|
||||||
|
_rotate_button.disabled = not selected
|
||||||
|
if _store_button != null:
|
||||||
|
_store_button.disabled = not selected
|
||||||
|
|
||||||
|
|
||||||
|
func report_status(message: String) -> void:
|
||||||
|
if _selection_label != null:
|
||||||
|
_selection_label.text = message
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
custom_minimum_size = Vector2(330.0, 0.0)
|
||||||
|
position = Vector2(925.0, 540.0)
|
||||||
|
z_index = 58
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
add_theme_stylebox_override(
|
||||||
|
"panel",
|
||||||
|
UtilityPageStyle.rounded_style(
|
||||||
|
Color(UtilityPageStyle.OCEAN_PANEL_DEEP, 0.96), 16
|
||||||
|
),
|
||||||
|
)
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
for side: StringName in [
|
||||||
|
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom",
|
||||||
|
]:
|
||||||
|
margin.add_theme_constant_override(side, 12)
|
||||||
|
add_child(margin)
|
||||||
|
var layout := VBoxContainer.new()
|
||||||
|
layout.add_theme_constant_override("separation", 8)
|
||||||
|
margin.add_child(layout)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "RV decor"
|
||||||
|
title.add_theme_font_size_override("font_size", 24)
|
||||||
|
layout.add_child(title)
|
||||||
|
_selection_label = Label.new()
|
||||||
|
_selection_label.text = "click decor to select; drag to move"
|
||||||
|
_selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_selection_label.add_theme_font_size_override("font_size", 17)
|
||||||
|
layout.add_child(_selection_label)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 8)
|
||||||
|
layout.add_child(row)
|
||||||
|
_mode_button = Button.new()
|
||||||
|
_mode_button.text = "placement: grid"
|
||||||
|
_mode_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
UtilityPageStyle.apply_ocean_button(_mode_button)
|
||||||
|
_mode_button.pressed.connect(_toggle_mode)
|
||||||
|
row.add_child(_mode_button)
|
||||||
|
_rotate_button = Button.new()
|
||||||
|
_rotate_button.text = "rotate"
|
||||||
|
_rotate_button.disabled = true
|
||||||
|
UtilityPageStyle.apply_ocean_button(_rotate_button)
|
||||||
|
_rotate_button.pressed.connect(rotate_requested.emit)
|
||||||
|
row.add_child(_rotate_button)
|
||||||
|
var action_row := HBoxContainer.new()
|
||||||
|
action_row.add_theme_constant_override("separation", 8)
|
||||||
|
layout.add_child(action_row)
|
||||||
|
_store_button = Button.new()
|
||||||
|
_store_button.text = "pack up"
|
||||||
|
_store_button.disabled = true
|
||||||
|
_store_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
UtilityPageStyle.apply_ocean_button(_store_button)
|
||||||
|
_store_button.pressed.connect(store_requested.emit)
|
||||||
|
action_row.add_child(_store_button)
|
||||||
|
var cancel_button := Button.new()
|
||||||
|
cancel_button.text = "clear selection"
|
||||||
|
cancel_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
UtilityPageStyle.apply_ocean_button(cancel_button)
|
||||||
|
cancel_button.pressed.connect(cancel_requested.emit)
|
||||||
|
action_row.add_child(cancel_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_mode() -> void:
|
||||||
|
_mode = (
|
||||||
|
PlayerHomeState.PlacementMode.FREE
|
||||||
|
if _mode == PlayerHomeState.PlacementMode.GRID
|
||||||
|
else PlayerHomeState.PlacementMode.GRID
|
||||||
|
)
|
||||||
|
set_mode(_mode)
|
||||||
|
placement_mode_requested.emit(int(_mode))
|
||||||
1
ui/home_decor_toolbar.gd.uid
Normal file
1
ui/home_decor_toolbar.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bmtlixw3wief3
|
||||||
8
ui/home_decor_toolbar.tscn
Normal file
8
ui/home_decor_toolbar.tscn
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/home_decor_toolbar.gd" id="1_script"]
|
||||||
|
|
||||||
|
[node name="HomeDecorToolbar" type="PanelContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
script = ExtResource("1_script")
|
||||||
11
ui/hotbar.gd
11
ui/hotbar.gd
|
|
@ -37,6 +37,7 @@ var _slots: Array[BubbleHotbarSlotType] = []
|
||||||
var _gameplay_input_enabled: bool = false
|
var _gameplay_input_enabled: bool = false
|
||||||
var _drag_enabled: bool = false
|
var _drag_enabled: bool = false
|
||||||
var _swap_hotbar_camera_scroll: bool = false
|
var _swap_hotbar_camera_scroll: bool = false
|
||||||
|
var _rv_interior_camera_mode: bool = false
|
||||||
var _motion_elapsed: float = 0.0
|
var _motion_elapsed: float = 0.0
|
||||||
var _compact_layout: bool = false
|
var _compact_layout: bool = false
|
||||||
var _player_menu_context: bool = false
|
var _player_menu_context: bool = false
|
||||||
|
|
@ -99,6 +100,10 @@ func set_swap_hotbar_camera_scroll(enabled: bool) -> void:
|
||||||
_swap_hotbar_camera_scroll = enabled
|
_swap_hotbar_camera_scroll = enabled
|
||||||
|
|
||||||
|
|
||||||
|
func set_rv_interior_camera_mode(enabled: bool) -> void:
|
||||||
|
_rv_interior_camera_mode = enabled
|
||||||
|
|
||||||
|
|
||||||
func set_drag_enabled(enabled: bool) -> void:
|
func set_drag_enabled(enabled: bool) -> void:
|
||||||
_drag_enabled = enabled
|
_drag_enabled = enabled
|
||||||
for slot: BubbleHotbarSlotType in _slots:
|
for slot: BubbleHotbarSlotType in _slots:
|
||||||
|
|
@ -308,7 +313,11 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
if (
|
if (
|
||||||
event is InputEventMouseButton
|
event is InputEventMouseButton
|
||||||
and event.pressed
|
and event.pressed
|
||||||
and event.shift_pressed == _swap_hotbar_camera_scroll
|
and (
|
||||||
|
event.shift_pressed
|
||||||
|
if _rv_interior_camera_mode
|
||||||
|
else event.shift_pressed == _swap_hotbar_camera_scroll
|
||||||
|
)
|
||||||
):
|
):
|
||||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||||
_hotbar.cycle_selection(-1)
|
_hotbar.cycle_selection(-1)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ class_name LogbookCatalog
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
|
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
|
const CalendarSeasonType = preload("res://world/calendar_season.gd")
|
||||||
|
|
||||||
enum Category {
|
enum Category {
|
||||||
FRESH_WATER,
|
FRESH_WATER,
|
||||||
|
|
@ -10,6 +13,66 @@ enum Category {
|
||||||
SHELLFISH,
|
SHELLFISH,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum DiscoveryFilter {
|
||||||
|
ALL,
|
||||||
|
CAUGHT,
|
||||||
|
MISSING,
|
||||||
|
INCOMPLETE,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SortMode {
|
||||||
|
CATALOG_NUMBER,
|
||||||
|
NAME,
|
||||||
|
RARITY,
|
||||||
|
CATCH_COUNT,
|
||||||
|
QUALITY_PROGRESS,
|
||||||
|
VALUE,
|
||||||
|
WEIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AvailabilityFilter {
|
||||||
|
ANY,
|
||||||
|
AVAILABLE_NOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SeasonFilter {
|
||||||
|
ANY,
|
||||||
|
CURRENT,
|
||||||
|
SPRING,
|
||||||
|
SUMMER,
|
||||||
|
FALL,
|
||||||
|
WINTER,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TimeFilter {
|
||||||
|
ANY,
|
||||||
|
CURRENT,
|
||||||
|
DAY,
|
||||||
|
NIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RarityFilter {
|
||||||
|
ANY,
|
||||||
|
COMMON,
|
||||||
|
UNCOMMON,
|
||||||
|
RARE,
|
||||||
|
EPIC,
|
||||||
|
LEGENDARY,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MethodFilter {
|
||||||
|
ANY,
|
||||||
|
FISHING,
|
||||||
|
NET,
|
||||||
|
DIGGING,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HoldingFilter {
|
||||||
|
ANY,
|
||||||
|
IN_COOLER,
|
||||||
|
FAVORITED,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static func category_for(fish: FishDataType) -> Category:
|
static func category_for(fish: FishDataType) -> Category:
|
||||||
if fish == null:
|
if fish == null:
|
||||||
|
|
@ -73,3 +136,278 @@ static func ordered_species(
|
||||||
return a.catalog_number < b.catalog_number
|
return a.catalog_number < b.catalog_number
|
||||||
)
|
)
|
||||||
return ordered
|
return ordered
|
||||||
|
|
||||||
|
|
||||||
|
static func filtered_species(
|
||||||
|
candidates: Array[FishDataType],
|
||||||
|
category: Category,
|
||||||
|
collection_log: CollectionLogType,
|
||||||
|
inventory: FishInventoryType,
|
||||||
|
options: Dictionary,
|
||||||
|
available_now_resolver: Callable = Callable(),
|
||||||
|
current_season: int = CalendarSeasonType.UNKNOWN,
|
||||||
|
current_is_night: bool = false,
|
||||||
|
) -> Array[FishDataType]:
|
||||||
|
var filtered: Array[FishDataType] = []
|
||||||
|
for fish: FishDataType in ordered_species(candidates):
|
||||||
|
if category_for(fish) != category:
|
||||||
|
continue
|
||||||
|
if not _matches_filters(
|
||||||
|
fish,
|
||||||
|
collection_log,
|
||||||
|
inventory,
|
||||||
|
options,
|
||||||
|
available_now_resolver,
|
||||||
|
current_season,
|
||||||
|
current_is_night,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
filtered.append(fish)
|
||||||
|
var sort_mode := int(options.get("sort", SortMode.CATALOG_NUMBER))
|
||||||
|
var descending := bool(options.get("descending", false))
|
||||||
|
filtered.sort_custom(
|
||||||
|
func(first: FishDataType, second: FishDataType) -> bool:
|
||||||
|
return _species_less_than(
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
collection_log,
|
||||||
|
sort_mode,
|
||||||
|
descending,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
static func available_groups(
|
||||||
|
candidates: Array[FishDataType],
|
||||||
|
category: Category,
|
||||||
|
_collection_log: CollectionLogType,
|
||||||
|
) -> Array[String]:
|
||||||
|
var groups: Array[String] = []
|
||||||
|
for fish: FishDataType in ordered_species(candidates):
|
||||||
|
if category_for(fish) != category:
|
||||||
|
continue
|
||||||
|
var group := _group_label(fish)
|
||||||
|
if not group.is_empty() and group not in groups:
|
||||||
|
groups.append(group)
|
||||||
|
groups.sort_custom(
|
||||||
|
func(first: String, second: String) -> bool:
|
||||||
|
return first.naturalnocasecmp_to(second) < 0
|
||||||
|
)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
static func quality_progress(
|
||||||
|
collection_log: CollectionLogType,
|
||||||
|
fish_id: StringName,
|
||||||
|
) -> int:
|
||||||
|
if collection_log == null:
|
||||||
|
return 0
|
||||||
|
var mask: int = collection_log.get_quality_mask(fish_id)
|
||||||
|
var count: int = 0
|
||||||
|
while mask > 0:
|
||||||
|
count += mask & 1
|
||||||
|
mask >>= 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
static func _matches_filters(
|
||||||
|
fish: FishDataType,
|
||||||
|
collection_log: CollectionLogType,
|
||||||
|
inventory: FishInventoryType,
|
||||||
|
options: Dictionary,
|
||||||
|
available_now_resolver: Callable,
|
||||||
|
current_season: int,
|
||||||
|
current_is_night: bool,
|
||||||
|
) -> bool:
|
||||||
|
var discovered := (
|
||||||
|
collection_log != null and collection_log.has_discovered(fish.id)
|
||||||
|
)
|
||||||
|
match int(options.get("show", DiscoveryFilter.ALL)):
|
||||||
|
DiscoveryFilter.CAUGHT:
|
||||||
|
if not discovered:
|
||||||
|
return false
|
||||||
|
DiscoveryFilter.MISSING:
|
||||||
|
if discovered:
|
||||||
|
return false
|
||||||
|
DiscoveryFilter.INCOMPLETE:
|
||||||
|
if (
|
||||||
|
not discovered
|
||||||
|
or collection_log.has_mastered(fish.id)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var search_text := str(options.get("search", "")).strip_edges().to_lower()
|
||||||
|
if not search_text.is_empty():
|
||||||
|
var number_text := str(catalog_number(fish))
|
||||||
|
var number_query := search_text.trim_prefix("#")
|
||||||
|
var number_matches := (
|
||||||
|
number_query.is_valid_int()
|
||||||
|
and number_text == number_query
|
||||||
|
)
|
||||||
|
var identity_matches := false
|
||||||
|
if discovered:
|
||||||
|
identity_matches = " ".join([
|
||||||
|
fish.display_name,
|
||||||
|
_group_label(fish),
|
||||||
|
fish.get_habitat_label(),
|
||||||
|
]).to_lower().contains(search_text)
|
||||||
|
if not number_matches and not identity_matches:
|
||||||
|
return false
|
||||||
|
|
||||||
|
if (
|
||||||
|
int(options.get("availability", AvailabilityFilter.ANY))
|
||||||
|
== AvailabilityFilter.AVAILABLE_NOW
|
||||||
|
):
|
||||||
|
if available_now_resolver.is_valid():
|
||||||
|
var resolved: Variant = available_now_resolver.call(fish)
|
||||||
|
if typeof(resolved) != TYPE_BOOL or not bool(resolved):
|
||||||
|
return false
|
||||||
|
elif not _matches_season(fish, current_season):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var season_filter := int(options.get("season", SeasonFilter.ANY))
|
||||||
|
if season_filter != SeasonFilter.ANY:
|
||||||
|
var requested_season := current_season
|
||||||
|
if season_filter >= SeasonFilter.SPRING:
|
||||||
|
requested_season = season_filter - SeasonFilter.SPRING
|
||||||
|
if not _matches_season(fish, requested_season):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var time_filter := int(options.get("time", TimeFilter.ANY))
|
||||||
|
if time_filter != TimeFilter.ANY:
|
||||||
|
var requested_night := current_is_night
|
||||||
|
if time_filter == TimeFilter.DAY:
|
||||||
|
requested_night = false
|
||||||
|
elif time_filter == TimeFilter.NIGHT:
|
||||||
|
requested_night = true
|
||||||
|
if not _matches_time(fish, requested_night):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var rarity_filter := int(options.get("rarity", RarityFilter.ANY))
|
||||||
|
if (
|
||||||
|
rarity_filter != RarityFilter.ANY
|
||||||
|
and fish.rarity != rarity_filter - RarityFilter.COMMON
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var method_filter := int(options.get("method", MethodFilter.ANY))
|
||||||
|
if (
|
||||||
|
method_filter != MethodFilter.ANY
|
||||||
|
and fish.collection_method != method_filter - MethodFilter.FISHING
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var holding_filter := int(options.get("holding", HoldingFilter.ANY))
|
||||||
|
if holding_filter != HoldingFilter.ANY:
|
||||||
|
var favorites_only := holding_filter == HoldingFilter.FAVORITED
|
||||||
|
if not _has_current_catch(inventory, fish.id, favorites_only):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var requested_group := str(options.get("group", ""))
|
||||||
|
if (
|
||||||
|
not requested_group.is_empty()
|
||||||
|
and _group_label(fish).nocasecmp_to(requested_group) != 0
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
static func _species_less_than(
|
||||||
|
first: FishDataType,
|
||||||
|
second: FishDataType,
|
||||||
|
collection_log: CollectionLogType,
|
||||||
|
sort_mode: int,
|
||||||
|
descending: bool,
|
||||||
|
) -> bool:
|
||||||
|
var first_discovered := (
|
||||||
|
collection_log != null and collection_log.has_discovered(first.id)
|
||||||
|
)
|
||||||
|
var second_discovered := (
|
||||||
|
collection_log != null and collection_log.has_discovered(second.id)
|
||||||
|
)
|
||||||
|
if sort_mode != SortMode.CATALOG_NUMBER:
|
||||||
|
if first_discovered != second_discovered:
|
||||||
|
return first_discovered
|
||||||
|
if not first_discovered and not second_discovered:
|
||||||
|
return _catalog_less_than(first, second)
|
||||||
|
|
||||||
|
var comparison: int = 0
|
||||||
|
match sort_mode:
|
||||||
|
SortMode.NAME:
|
||||||
|
comparison = first.display_name.naturalnocasecmp_to(
|
||||||
|
second.display_name
|
||||||
|
)
|
||||||
|
SortMode.RARITY:
|
||||||
|
comparison = first.rarity - second.rarity
|
||||||
|
SortMode.CATCH_COUNT:
|
||||||
|
comparison = (
|
||||||
|
collection_log.get_catch_count(first.id)
|
||||||
|
- collection_log.get_catch_count(second.id)
|
||||||
|
)
|
||||||
|
SortMode.QUALITY_PROGRESS:
|
||||||
|
comparison = (
|
||||||
|
quality_progress(collection_log, first.id)
|
||||||
|
- quality_progress(collection_log, second.id)
|
||||||
|
)
|
||||||
|
SortMode.VALUE:
|
||||||
|
comparison = first.sell_value_max - second.sell_value_max
|
||||||
|
SortMode.WEIGHT:
|
||||||
|
comparison = _compare_floats(
|
||||||
|
first.get_maximum_weight(),
|
||||||
|
second.get_maximum_weight(),
|
||||||
|
)
|
||||||
|
_:
|
||||||
|
comparison = first.catalog_number - second.catalog_number
|
||||||
|
if comparison != 0:
|
||||||
|
return comparison > 0 if descending else comparison < 0
|
||||||
|
return _catalog_less_than(first, second)
|
||||||
|
|
||||||
|
|
||||||
|
static func _catalog_less_than(first: FishDataType, second: FishDataType) -> bool:
|
||||||
|
if first.catalog_number == second.catalog_number:
|
||||||
|
return String(first.id) < String(second.id)
|
||||||
|
return first.catalog_number < second.catalog_number
|
||||||
|
|
||||||
|
|
||||||
|
static func _compare_floats(first: float, second: float) -> int:
|
||||||
|
if is_equal_approx(first, second):
|
||||||
|
return 0
|
||||||
|
return -1 if first < second else 1
|
||||||
|
|
||||||
|
|
||||||
|
static func _matches_season(fish: FishDataType, season: int) -> bool:
|
||||||
|
return fish.is_available_in_season(season)
|
||||||
|
|
||||||
|
|
||||||
|
static func _matches_time(fish: FishDataType, requested_night: bool) -> bool:
|
||||||
|
if (
|
||||||
|
fish.collection_method != FishDataType.CollectionMethod.FISHING
|
||||||
|
or fish.availability == null
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
return (
|
||||||
|
fish.availability.allow_night
|
||||||
|
if requested_night
|
||||||
|
else fish.availability.allow_day
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _has_current_catch(
|
||||||
|
inventory: FishInventoryType,
|
||||||
|
fish_id: StringName,
|
||||||
|
favorites_only: bool,
|
||||||
|
) -> bool:
|
||||||
|
if inventory == null:
|
||||||
|
return false
|
||||||
|
for fish_catch in inventory.get_catches_by_fish_id(fish_id):
|
||||||
|
if fish_catch != null and (not favorites_only or fish_catch.is_favorited):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
static func _group_label(fish: FishDataType) -> String:
|
||||||
|
if fish == null:
|
||||||
|
return ""
|
||||||
|
var group := String(fish.collection_group).strip_edges()
|
||||||
|
return group if not group.is_empty() else fish.get_habitat_label()
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,21 @@ const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
|
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||||
|
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||||
|
const WorldWeatherServiceType = preload(
|
||||||
|
"res://world/world_weather_service.gd"
|
||||||
|
)
|
||||||
const SILHOUETTE_SHADER: Shader = preload(
|
const SILHOUETTE_SHADER: Shader = preload(
|
||||||
"res://ui/logbook_silhouette.gdshader"
|
"res://ui/logbook_silhouette.gdshader"
|
||||||
)
|
)
|
||||||
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
|
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
|
||||||
|
const ControllerFocusNavigationType = preload(
|
||||||
|
"res://ui/controller_focus_navigation.gd"
|
||||||
|
)
|
||||||
|
const NOTEPAD_INK_ACTION_SCENE: PackedScene = preload(
|
||||||
|
"res://ui/components/bubble_menu/notepad_ink_action.tscn"
|
||||||
|
)
|
||||||
const InventoryNotepadType = preload(
|
const InventoryNotepadType = preload(
|
||||||
"res://ui/components/inventory_notepad.gd"
|
"res://ui/components/inventory_notepad.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -48,6 +59,8 @@ const LOGBOOK_TAB_LEFT_INSET: float = 92.0
|
||||||
const LOGBOOK_TAB_SIZE := Vector2(108.0, 38.0)
|
const LOGBOOK_TAB_SIZE := Vector2(108.0, 38.0)
|
||||||
const LOGBOOK_TAB_SEPARATION: float = 6.0
|
const LOGBOOK_TAB_SEPARATION: float = 6.0
|
||||||
const LOGBOOK_TAB_TEXT_SIDE_INSET: float = 10.0
|
const LOGBOOK_TAB_TEXT_SIDE_INSET: float = 10.0
|
||||||
|
const INDEX_TAB_SIZE := Vector2(88.0, 38.0)
|
||||||
|
const INDEX_TAB_RIGHT_INSET: float = 72.0
|
||||||
const PAGE_CONTENT_SCALE: float = 0.97
|
const PAGE_CONTENT_SCALE: float = 0.97
|
||||||
const WIDE_OUTER_MARGINS := Rect2(52.0, 85.0, 52.0, 18.0)
|
const WIDE_OUTER_MARGINS := Rect2(52.0, 85.0, 52.0, 18.0)
|
||||||
const COMPACT_OUTER_MARGINS := Rect2(160.0, 85.0, 160.0, 106.0)
|
const COMPACT_OUTER_MARGINS := Rect2(160.0, 85.0, 160.0, 106.0)
|
||||||
|
|
@ -61,17 +74,53 @@ const DETAIL_SECTION_SEPARATION: int = 10
|
||||||
const DETAIL_QUALITY_SECTION_SIZE := Vector2(300.0, 104.0)
|
const DETAIL_QUALITY_SECTION_SIZE := Vector2(300.0, 104.0)
|
||||||
const DETAIL_STATS_SECTION_SIZE := Vector2(400.0, 160.0)
|
const DETAIL_STATS_SECTION_SIZE := Vector2(400.0, 160.0)
|
||||||
const DETAIL_STATS_ROW_SEPARATION: int = 8
|
const DETAIL_STATS_ROW_SEPARATION: int = 8
|
||||||
|
const INDEX_OPTION_HEIGHT: float = 34.0
|
||||||
|
const INDEX_CONTENT_LEFT_INSET: float = 44.0
|
||||||
|
const INDEX_CONTENT_TOP_INSET: float = 4.0
|
||||||
|
const INDEX_CONTENT_RIGHT_INSET: float = 22.0
|
||||||
|
const INDEX_CONTENT_BOTTOM_INSET: float = 10.0
|
||||||
|
const INDEX_SHOW_LABELS := [
|
||||||
|
"all entries", "caught", "missing", "needs quality",
|
||||||
|
]
|
||||||
|
const INDEX_SORT_LABELS := [
|
||||||
|
"catalog no.", "name", "rarity", "number caught",
|
||||||
|
"quality progress", "value", "weight",
|
||||||
|
]
|
||||||
|
const INDEX_AVAILABILITY_LABELS := [
|
||||||
|
"any time", "available now",
|
||||||
|
]
|
||||||
|
const INDEX_SEASON_LABELS := [
|
||||||
|
"any season", "current season", "spring", "summer", "fall", "winter",
|
||||||
|
]
|
||||||
|
const INDEX_TIME_LABELS := [
|
||||||
|
"any time", "current time", "day", "night",
|
||||||
|
]
|
||||||
|
const INDEX_RARITY_LABELS := [
|
||||||
|
"any rarity", "common", "uncommon", "rare", "epic", "legendary",
|
||||||
|
]
|
||||||
|
const INDEX_METHOD_LABELS := [
|
||||||
|
"any method", "fishing", "netting", "digging",
|
||||||
|
]
|
||||||
|
const INDEX_HOLDING_LABELS := [
|
||||||
|
"any ownership", "in cooler", "favorited",
|
||||||
|
]
|
||||||
|
|
||||||
|
signal controller_text_entry_requested(control: Control)
|
||||||
|
|
||||||
enum ControllerZone {
|
enum ControllerZone {
|
||||||
TABS,
|
TABS,
|
||||||
ENTRIES,
|
ENTRIES,
|
||||||
DETAILS,
|
DETAILS,
|
||||||
|
INDEX,
|
||||||
OVERLAY,
|
OVERLAY,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _collection_log: CollectionLogType
|
var _collection_log: CollectionLogType
|
||||||
var _inventory: FishInventoryType
|
var _inventory: FishInventoryType
|
||||||
var _catalog: FishPoolType
|
var _catalog: FishPoolType
|
||||||
|
var _fishing_spot: FishingSpotType
|
||||||
|
var _world_time: WorldTimeServiceType
|
||||||
|
var _world_weather: WorldWeatherServiceType
|
||||||
var _category: LogbookCatalog.Category = (
|
var _category: LogbookCatalog.Category = (
|
||||||
LogbookCatalog.Category.FRESH_WATER
|
LogbookCatalog.Category.FRESH_WATER
|
||||||
)
|
)
|
||||||
|
|
@ -111,6 +160,31 @@ var _portrait_overlay_stats_view: Control
|
||||||
var _portrait_overlay_stats: GridContainer
|
var _portrait_overlay_stats: GridContainer
|
||||||
var _overlay_return_focus: Control
|
var _overlay_return_focus: Control
|
||||||
var _controller_zone: ControllerZone = ControllerZone.TABS
|
var _controller_zone: ControllerZone = ControllerZone.TABS
|
||||||
|
var _index_tab: OrganizerTabType
|
||||||
|
var _index_body: VBoxContainer
|
||||||
|
var _index_search: LineEdit
|
||||||
|
var _index_summary: Label
|
||||||
|
var _index_note: Label
|
||||||
|
var _index_reset: Button
|
||||||
|
var _index_option_buttons: Dictionary[StringName, Button] = {}
|
||||||
|
var _index_controls: Array[Control] = []
|
||||||
|
var _index_open: bool = false
|
||||||
|
var _index_update_in_progress: bool = false
|
||||||
|
var _index_visible_count: int = 0
|
||||||
|
var _index_category_total: int = 0
|
||||||
|
var _index_options: Dictionary = {
|
||||||
|
"search": "",
|
||||||
|
"show": LogbookCatalog.DiscoveryFilter.ALL,
|
||||||
|
"sort": LogbookCatalog.SortMode.CATALOG_NUMBER,
|
||||||
|
"descending": false,
|
||||||
|
"availability": LogbookCatalog.AvailabilityFilter.ANY,
|
||||||
|
"season": LogbookCatalog.SeasonFilter.ANY,
|
||||||
|
"time": LogbookCatalog.TimeFilter.ANY,
|
||||||
|
"rarity": LogbookCatalog.RarityFilter.ANY,
|
||||||
|
"method": LogbookCatalog.MethodFilter.ANY,
|
||||||
|
"holding": LogbookCatalog.HoldingFilter.ANY,
|
||||||
|
"group": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -124,10 +198,16 @@ func setup(
|
||||||
collection_log: CollectionLogType,
|
collection_log: CollectionLogType,
|
||||||
inventory: FishInventoryType,
|
inventory: FishInventoryType,
|
||||||
catalog: FishPoolType,
|
catalog: FishPoolType,
|
||||||
|
fishing_spot: FishingSpotType = null,
|
||||||
|
world_time: WorldTimeServiceType = null,
|
||||||
|
world_weather: WorldWeatherServiceType = null,
|
||||||
) -> void:
|
) -> void:
|
||||||
_collection_log = collection_log
|
_collection_log = collection_log
|
||||||
_inventory = inventory
|
_inventory = inventory
|
||||||
_catalog = catalog
|
_catalog = catalog
|
||||||
|
_fishing_spot = fishing_spot
|
||||||
|
_world_time = world_time
|
||||||
|
_world_weather = world_weather
|
||||||
if not _collection_log.fish_discovered.is_connected(
|
if not _collection_log.fish_discovered.is_connected(
|
||||||
_on_fish_discovered
|
_on_fish_discovered
|
||||||
):
|
):
|
||||||
|
|
@ -138,12 +218,28 @@ func setup(
|
||||||
_collection_log.collection_changed.connect(_on_collection_changed)
|
_collection_log.collection_changed.connect(_on_collection_changed)
|
||||||
if not _inventory.catches_changed.is_connected(_on_inventory_changed):
|
if not _inventory.catches_changed.is_connected(_on_inventory_changed):
|
||||||
_inventory.catches_changed.connect(_on_inventory_changed)
|
_inventory.catches_changed.connect(_on_inventory_changed)
|
||||||
|
if (
|
||||||
|
_world_time != null
|
||||||
|
and not _world_time.time_changed.is_connected(
|
||||||
|
_on_logbook_context_changed
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_world_time.time_changed.connect(_on_logbook_context_changed)
|
||||||
|
_world_time.calendar_date_changed.connect(_on_logbook_context_changed)
|
||||||
|
if (
|
||||||
|
_world_weather != null
|
||||||
|
and not _world_weather.weather_changed.is_connected(
|
||||||
|
_on_logbook_context_changed
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_world_weather.weather_changed.connect(_on_logbook_context_changed)
|
||||||
_refresh_catalog()
|
_refresh_catalog()
|
||||||
|
|
||||||
|
|
||||||
func set_compact_presentation(compact: bool) -> void:
|
func set_compact_presentation(compact: bool) -> void:
|
||||||
_compact_presentation = compact
|
_compact_presentation = compact
|
||||||
_apply_presentation_margins()
|
_apply_presentation_margins()
|
||||||
|
_apply_index_presentation()
|
||||||
|
|
||||||
|
|
||||||
func activate() -> void:
|
func activate() -> void:
|
||||||
|
|
@ -180,6 +276,17 @@ func set_interactive(value: bool) -> void:
|
||||||
if _interactive and not tab.button_pressed
|
if _interactive and not tab.button_pressed
|
||||||
else Control.MOUSE_FILTER_IGNORE
|
else Control.MOUSE_FILTER_IGNORE
|
||||||
)
|
)
|
||||||
|
if _index_tab != null:
|
||||||
|
_index_tab.focus_mode = (
|
||||||
|
Control.FOCUS_ALL
|
||||||
|
if _interactive and _controller_zone == ControllerZone.TABS
|
||||||
|
else Control.FOCUS_NONE
|
||||||
|
)
|
||||||
|
_index_tab.mouse_filter = (
|
||||||
|
Control.MOUSE_FILTER_STOP
|
||||||
|
if _interactive
|
||||||
|
else Control.MOUSE_FILTER_IGNORE
|
||||||
|
)
|
||||||
for entry: Button in _entry_buttons.values():
|
for entry: Button in _entry_buttons.values():
|
||||||
entry.focus_mode = (
|
entry.focus_mode = (
|
||||||
Control.FOCUS_ALL
|
Control.FOCUS_ALL
|
||||||
|
|
@ -199,6 +306,19 @@ func set_interactive(value: bool) -> void:
|
||||||
if _interactive and _controller_zone == ControllerZone.DETAILS
|
if _interactive and _controller_zone == ControllerZone.DETAILS
|
||||||
else Control.FOCUS_NONE
|
else Control.FOCUS_NONE
|
||||||
)
|
)
|
||||||
|
for index_control: Control in _index_controls:
|
||||||
|
if not is_instance_valid(index_control):
|
||||||
|
continue
|
||||||
|
index_control.focus_mode = (
|
||||||
|
Control.FOCUS_ALL
|
||||||
|
if _interactive and _controller_zone == ControllerZone.INDEX
|
||||||
|
else Control.FOCUS_NONE
|
||||||
|
)
|
||||||
|
index_control.mouse_filter = (
|
||||||
|
Control.MOUSE_FILTER_STOP
|
||||||
|
if _interactive and _index_open
|
||||||
|
else Control.MOUSE_FILTER_IGNORE
|
||||||
|
)
|
||||||
if _portrait_overlay_backdrop != null:
|
if _portrait_overlay_backdrop != null:
|
||||||
_portrait_overlay_backdrop.focus_mode = (
|
_portrait_overlay_backdrop.focus_mode = (
|
||||||
Control.FOCUS_ALL
|
Control.FOCUS_ALL
|
||||||
|
|
@ -214,6 +334,9 @@ func focus_initial() -> void:
|
||||||
_portrait_overlay_backdrop.grab_focus()
|
_portrait_overlay_backdrop.grab_focus()
|
||||||
return
|
return
|
||||||
if _controller_zone == ControllerZone.TABS:
|
if _controller_zone == ControllerZone.TABS:
|
||||||
|
if _index_open and _index_tab != null:
|
||||||
|
_index_tab.grab_focus()
|
||||||
|
return
|
||||||
var category_tab_index: int = _category_tab_categories.find(_category)
|
var category_tab_index: int = _category_tab_categories.find(_category)
|
||||||
if category_tab_index >= 0:
|
if category_tab_index >= 0:
|
||||||
_category_tabs[category_tab_index].grab_focus()
|
_category_tabs[category_tab_index].grab_focus()
|
||||||
|
|
@ -222,6 +345,10 @@ func focus_initial() -> void:
|
||||||
if not _detail_buttons.is_empty():
|
if not _detail_buttons.is_empty():
|
||||||
_detail_buttons.front().grab_focus()
|
_detail_buttons.front().grab_focus()
|
||||||
return
|
return
|
||||||
|
if _controller_zone == ControllerZone.INDEX:
|
||||||
|
if _index_search != null:
|
||||||
|
_index_search.grab_focus()
|
||||||
|
return
|
||||||
var selected := _entry_buttons.get(_selected_entry_key) as Button
|
var selected := _entry_buttons.get(_selected_entry_key) as Button
|
||||||
if selected != null:
|
if selected != null:
|
||||||
selected.grab_focus()
|
selected.grab_focus()
|
||||||
|
|
@ -247,6 +374,8 @@ func handle_controller_input(event: InputEvent) -> bool:
|
||||||
_hide_portrait_overlay()
|
_hide_portrait_overlay()
|
||||||
ControllerZone.DETAILS:
|
ControllerZone.DETAILS:
|
||||||
_set_controller_zone(ControllerZone.ENTRIES)
|
_set_controller_zone(ControllerZone.ENTRIES)
|
||||||
|
ControllerZone.INDEX:
|
||||||
|
_set_index_open(false)
|
||||||
ControllerZone.ENTRIES:
|
ControllerZone.ENTRIES:
|
||||||
_set_controller_zone(ControllerZone.TABS)
|
_set_controller_zone(ControllerZone.TABS)
|
||||||
ControllerZone.TABS:
|
ControllerZone.TABS:
|
||||||
|
|
@ -254,15 +383,43 @@ func handle_controller_input(event: InputEvent) -> bool:
|
||||||
return true
|
return true
|
||||||
if _controller_zone == ControllerZone.TABS:
|
if _controller_zone == ControllerZone.TABS:
|
||||||
if event.is_action_pressed("ui_left"):
|
if event.is_action_pressed("ui_left"):
|
||||||
|
if _index_open:
|
||||||
|
_set_index_open(false, false)
|
||||||
|
_category_tabs.back().grab_focus()
|
||||||
|
return true
|
||||||
_select_adjacent_controller_category(-1)
|
_select_adjacent_controller_category(-1)
|
||||||
return true
|
return true
|
||||||
if event.is_action_pressed("ui_right"):
|
if event.is_action_pressed("ui_right"):
|
||||||
|
if (
|
||||||
|
_category_tab_categories.find(_category)
|
||||||
|
== _category_tab_categories.size() - 1
|
||||||
|
):
|
||||||
|
_set_index_open(true, false)
|
||||||
|
_index_tab.grab_focus()
|
||||||
|
return true
|
||||||
_select_adjacent_controller_category(1)
|
_select_adjacent_controller_category(1)
|
||||||
return true
|
return true
|
||||||
if accept_pressed:
|
if accept_pressed:
|
||||||
if not _entry_buttons.is_empty():
|
if _index_open:
|
||||||
|
_set_controller_zone(ControllerZone.INDEX)
|
||||||
|
elif not _entry_buttons.is_empty():
|
||||||
_set_controller_zone(ControllerZone.ENTRIES)
|
_set_controller_zone(ControllerZone.ENTRIES)
|
||||||
return true
|
return true
|
||||||
|
if _controller_zone == ControllerZone.INDEX:
|
||||||
|
var focus_owner := get_viewport().gui_get_focus_owner()
|
||||||
|
if accept_pressed and focus_owner == _index_search:
|
||||||
|
controller_text_entry_requested.emit(_index_search)
|
||||||
|
return true
|
||||||
|
if event.is_action_pressed("ui_left"):
|
||||||
|
var previous_key := _index_key_for_control(focus_owner)
|
||||||
|
if not previous_key.is_empty():
|
||||||
|
_cycle_index_option(previous_key, -1)
|
||||||
|
return true
|
||||||
|
if event.is_action_pressed("ui_right") or accept_pressed:
|
||||||
|
var next_key := _index_key_for_control(focus_owner)
|
||||||
|
if not next_key.is_empty():
|
||||||
|
_cycle_index_option(next_key, 1)
|
||||||
|
return true
|
||||||
if _controller_zone == ControllerZone.ENTRIES and accept_pressed:
|
if _controller_zone == ControllerZone.ENTRIES and accept_pressed:
|
||||||
if not _selected_entry_key.is_empty() and not _detail_buttons.is_empty():
|
if not _selected_entry_key.is_empty() and not _detail_buttons.is_empty():
|
||||||
_set_controller_zone(ControllerZone.DETAILS)
|
_set_controller_zone(ControllerZone.DETAILS)
|
||||||
|
|
@ -329,6 +486,20 @@ func _build_interface() -> void:
|
||||||
tabs.add_child(tab)
|
tabs.add_child(tab)
|
||||||
_category_tabs.append(tab)
|
_category_tabs.append(tab)
|
||||||
_category_tab_categories.append(category)
|
_category_tab_categories.append(category)
|
||||||
|
_index_tab = OrganizerTabType.new()
|
||||||
|
_index_tab.name = "CatalogIndexTab"
|
||||||
|
_index_tab.anchor_left = 1.0
|
||||||
|
_index_tab.anchor_right = 1.0
|
||||||
|
_index_tab.offset_left = -INDEX_TAB_RIGHT_INSET - INDEX_TAB_SIZE.x
|
||||||
|
_index_tab.offset_top = 37.0
|
||||||
|
_index_tab.offset_right = -INDEX_TAB_RIGHT_INSET
|
||||||
|
_index_tab.offset_bottom = 37.0 + INDEX_TAB_SIZE.y
|
||||||
|
_index_tab.text = "index"
|
||||||
|
_index_tab.palette_index = 0
|
||||||
|
_index_tab.tooltip_text = "Open catalog index"
|
||||||
|
_index_tab.accessibility_name = "Catalog index"
|
||||||
|
_index_tab.pressed.connect(_toggle_index_page)
|
||||||
|
stack.add_child(_index_tab)
|
||||||
_configure_category_focus()
|
_configure_category_focus()
|
||||||
|
|
||||||
var book := Control.new()
|
var book := Control.new()
|
||||||
|
|
@ -418,9 +589,176 @@ func _build_interface() -> void:
|
||||||
right_page.add_child(_detail_body)
|
right_page.add_child(_detail_body)
|
||||||
_apply_page_content_scale(_detail_body)
|
_apply_page_content_scale(_detail_body)
|
||||||
_show_no_selection()
|
_show_no_selection()
|
||||||
|
_build_index_page(right_page)
|
||||||
_build_portrait_overlay()
|
_build_portrait_overlay()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_index_page(right_page: Control) -> void:
|
||||||
|
_index_body = VBoxContainer.new()
|
||||||
|
_index_body.name = "CatalogIndex"
|
||||||
|
_index_body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_index_body.offset_left = INDEX_CONTENT_LEFT_INSET
|
||||||
|
_index_body.offset_top = INDEX_CONTENT_TOP_INSET
|
||||||
|
_index_body.offset_right = -INDEX_CONTENT_RIGHT_INSET
|
||||||
|
_index_body.offset_bottom = -INDEX_CONTENT_BOTTOM_INSET
|
||||||
|
_index_body.add_theme_constant_override("separation", 4)
|
||||||
|
_index_body.visible = false
|
||||||
|
right_page.add_child(_index_body)
|
||||||
|
_apply_page_content_scale(_index_body)
|
||||||
|
_apply_index_presentation()
|
||||||
|
|
||||||
|
var heading := _label("catalog index", 28)
|
||||||
|
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_index_body.add_child(heading)
|
||||||
|
_index_summary = _field_label("", 14)
|
||||||
|
_index_summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_index_summary.add_theme_color_override("font_color", MUTED_INK)
|
||||||
|
_index_body.add_child(_index_summary)
|
||||||
|
|
||||||
|
var search_stack := VBoxContainer.new()
|
||||||
|
search_stack.add_theme_constant_override("separation", 0)
|
||||||
|
_index_body.add_child(search_stack)
|
||||||
|
var search_label := _field_label("find by name, group, or catalog no.", 13)
|
||||||
|
search_label.add_theme_color_override("font_color", MUTED_INK)
|
||||||
|
search_stack.add_child(search_label)
|
||||||
|
_index_search = LineEdit.new()
|
||||||
|
_index_search.name = "CatalogIndexSearch"
|
||||||
|
_index_search.custom_minimum_size.y = 34.0
|
||||||
|
_index_search.placeholder_text = "write here..."
|
||||||
|
_index_search.max_length = 48
|
||||||
|
_index_search.add_theme_font_override(
|
||||||
|
"font", InventoryNotepadType.HANDWRITTEN_FONT
|
||||||
|
)
|
||||||
|
_index_search.add_theme_font_size_override("font_size", 18)
|
||||||
|
_index_search.add_theme_color_override("font_color", INK)
|
||||||
|
_index_search.add_theme_color_override("caret_color", INK)
|
||||||
|
_index_search.add_theme_color_override(
|
||||||
|
"font_placeholder_color", Color(MUTED_INK, 0.72)
|
||||||
|
)
|
||||||
|
for style_name: StringName in [
|
||||||
|
&"normal", &"focus", &"read_only",
|
||||||
|
]:
|
||||||
|
_index_search.add_theme_stylebox_override(
|
||||||
|
style_name,
|
||||||
|
_index_search_style(style_name == &"focus"),
|
||||||
|
)
|
||||||
|
_index_search.text_changed.connect(_on_index_search_changed)
|
||||||
|
search_stack.add_child(_index_search)
|
||||||
|
_index_controls.append(_index_search)
|
||||||
|
|
||||||
|
var options_grid := GridContainer.new()
|
||||||
|
options_grid.columns = 2
|
||||||
|
options_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
options_grid.add_theme_constant_override("h_separation", 18)
|
||||||
|
options_grid.add_theme_constant_override("v_separation", 2)
|
||||||
|
_index_body.add_child(options_grid)
|
||||||
|
for descriptor: Dictionary in [
|
||||||
|
{"key": &"show", "label": "show"},
|
||||||
|
{"key": &"rarity", "label": "rarity"},
|
||||||
|
{"key": &"sort", "label": "sort by"},
|
||||||
|
{"key": &"availability", "label": "availability"},
|
||||||
|
{"key": &"descending", "label": "order"},
|
||||||
|
{"key": &"season", "label": "season"},
|
||||||
|
{"key": &"method", "label": "method"},
|
||||||
|
{"key": &"time", "label": "time of day"},
|
||||||
|
{"key": &"holding", "label": "ownership"},
|
||||||
|
{"key": &"group", "label": "collection group"},
|
||||||
|
]:
|
||||||
|
options_grid.add_child(_make_index_option(
|
||||||
|
descriptor["key"],
|
||||||
|
str(descriptor["label"]),
|
||||||
|
))
|
||||||
|
|
||||||
|
var footer := HBoxContainer.new()
|
||||||
|
footer.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
_index_body.add_child(footer)
|
||||||
|
_index_reset = NOTEPAD_INK_ACTION_SCENE.instantiate() as Button
|
||||||
|
_index_reset.name = "CatalogIndexReset"
|
||||||
|
_index_reset.custom_minimum_size = Vector2(126.0, 34.0)
|
||||||
|
_index_reset.text = "clear index"
|
||||||
|
_apply_index_value_font(_index_reset)
|
||||||
|
_index_reset.tooltip_text = "Clear every catalog filter"
|
||||||
|
_index_reset.accessibility_name = "Clear catalog index filters"
|
||||||
|
_index_reset.pressed.connect(_reset_index)
|
||||||
|
footer.add_child(_index_reset)
|
||||||
|
_index_controls.append(_index_reset)
|
||||||
|
|
||||||
|
_index_note = _field_label(
|
||||||
|
"silhouettes keep names hidden in filtered results",
|
||||||
|
12,
|
||||||
|
)
|
||||||
|
_index_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_index_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_index_note.add_theme_color_override("font_color", Color(MUTED_INK, 0.82))
|
||||||
|
_index_body.add_child(_index_note)
|
||||||
|
_update_index_controls()
|
||||||
|
_configure_index_focus.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _make_index_option(key: StringName, label_text: String) -> Control:
|
||||||
|
var stack := VBoxContainer.new()
|
||||||
|
stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
stack.add_theme_constant_override("separation", -3)
|
||||||
|
var label := _field_label(label_text, 12)
|
||||||
|
label.add_theme_color_override("font_color", MUTED_INK)
|
||||||
|
stack.add_child(label)
|
||||||
|
var button := NOTEPAD_INK_ACTION_SCENE.instantiate() as Button
|
||||||
|
button.name = "CatalogIndex%s" % String(key).capitalize().replace(" ", "")
|
||||||
|
button.custom_minimum_size = Vector2(0.0, INDEX_OPTION_HEIGHT)
|
||||||
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
button.clip_text = true
|
||||||
|
_apply_index_value_font(button)
|
||||||
|
button.tooltip_text = "Change %s; use left or right on a controller" % label_text
|
||||||
|
button.pressed.connect(_cycle_index_option.bind(key, 1))
|
||||||
|
button.gui_input.connect(_on_index_option_gui_input.bind(key))
|
||||||
|
stack.add_child(button)
|
||||||
|
_index_option_buttons[key] = button
|
||||||
|
_index_controls.append(button)
|
||||||
|
return stack
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_index_value_font(button: Button) -> void:
|
||||||
|
button.add_theme_font_override(
|
||||||
|
"font", InventoryNotepadType.HANDWRITTEN_FONT
|
||||||
|
)
|
||||||
|
button.add_theme_font_size_override("font_size", 18)
|
||||||
|
for color_name: StringName in [
|
||||||
|
&"font_color",
|
||||||
|
&"font_hover_color",
|
||||||
|
&"font_focus_color",
|
||||||
|
&"font_pressed_color",
|
||||||
|
]:
|
||||||
|
button.add_theme_color_override(color_name, INK)
|
||||||
|
|
||||||
|
|
||||||
|
func _index_search_style(focused: bool) -> StyleBoxFlat:
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color(1.0, 0.97, 0.82, 0.12 if focused else 0.0)
|
||||||
|
style.border_color = Color(INK, 0.72 if focused else 0.42)
|
||||||
|
style.border_width_bottom = 2
|
||||||
|
style.content_margin_left = 6
|
||||||
|
style.content_margin_right = 22
|
||||||
|
style.content_margin_top = 2
|
||||||
|
style.content_margin_bottom = 3
|
||||||
|
return style
|
||||||
|
|
||||||
|
|
||||||
|
func _configure_index_focus() -> void:
|
||||||
|
var controls: Array[Control] = []
|
||||||
|
for control: Control in _index_controls:
|
||||||
|
if control != null and is_instance_valid(control):
|
||||||
|
controls.append(control)
|
||||||
|
if not controls.is_empty():
|
||||||
|
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_index_presentation() -> void:
|
||||||
|
if _index_body == null:
|
||||||
|
return
|
||||||
|
var scale_value := 0.82 if _compact_presentation else PAGE_CONTENT_SCALE
|
||||||
|
_index_body.scale = Vector2.ONE * scale_value
|
||||||
|
|
||||||
|
|
||||||
func _apply_presentation_margins() -> void:
|
func _apply_presentation_margins() -> void:
|
||||||
if _outer_margin == null:
|
if _outer_margin == null:
|
||||||
return
|
return
|
||||||
|
|
@ -548,11 +886,30 @@ func _refresh_catalog() -> void:
|
||||||
var preserved_entry_key: StringName = _selected_entry_key
|
var preserved_entry_key: StringName = _selected_entry_key
|
||||||
_clear_entries()
|
_clear_entries()
|
||||||
var species: Array[FishDataType] = []
|
var species: Array[FishDataType] = []
|
||||||
|
var category_total: int = 0
|
||||||
if _catalog != null:
|
if _catalog != null:
|
||||||
species = LogbookCatalog.ordered_species(_catalog.candidates)
|
for fish: FishDataType in LogbookCatalog.ordered_species(
|
||||||
|
_catalog.candidates
|
||||||
|
):
|
||||||
|
if LogbookCatalog.category_for(fish) == _category:
|
||||||
|
category_total += 1
|
||||||
|
var available_now_resolver := Callable()
|
||||||
|
if _fishing_spot != null and is_instance_valid(_fishing_spot):
|
||||||
|
available_now_resolver = Callable(
|
||||||
|
_fishing_spot,
|
||||||
|
"is_species_available_now",
|
||||||
|
)
|
||||||
|
species = LogbookCatalog.filtered_species(
|
||||||
|
_catalog.candidates,
|
||||||
|
_category,
|
||||||
|
_collection_log,
|
||||||
|
_inventory,
|
||||||
|
_index_options,
|
||||||
|
available_now_resolver,
|
||||||
|
_current_season(),
|
||||||
|
_current_is_night(),
|
||||||
|
)
|
||||||
for fish: FishDataType in species:
|
for fish: FishDataType in species:
|
||||||
if LogbookCatalog.category_for(fish) != _category:
|
|
||||||
continue
|
|
||||||
var discovered: bool = _collection_log.has_discovered(fish.id)
|
var discovered: bool = _collection_log.has_discovered(fish.id)
|
||||||
var entry: Button = _make_entry(fish, discovered)
|
var entry: Button = _make_entry(fish, discovered)
|
||||||
var selection_key: StringName = (
|
var selection_key: StringName = (
|
||||||
|
|
@ -572,7 +929,11 @@ func _refresh_catalog() -> void:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
_catalog_grid.add_child(entry)
|
_catalog_grid.add_child(entry)
|
||||||
_empty_state.text = LogbookCatalog.empty_state(_category)
|
_empty_state.text = (
|
||||||
|
"No entries match this index."
|
||||||
|
if _index_has_active_criteria()
|
||||||
|
else LogbookCatalog.empty_state(_category)
|
||||||
|
)
|
||||||
_empty_state.visible = _entry_buttons.is_empty()
|
_empty_state.visible = _entry_buttons.is_empty()
|
||||||
_catalog_scroll.visible = not _entry_buttons.is_empty()
|
_catalog_scroll.visible = not _entry_buttons.is_empty()
|
||||||
if (
|
if (
|
||||||
|
|
@ -589,10 +950,269 @@ func _refresh_catalog() -> void:
|
||||||
_selected_entry_key = StringName()
|
_selected_entry_key = StringName()
|
||||||
_refresh_selection_styles()
|
_refresh_selection_styles()
|
||||||
_refresh_details(false)
|
_refresh_details(false)
|
||||||
|
_update_index_controls(species.size(), category_total)
|
||||||
set_interactive(_interactive)
|
set_interactive(_interactive)
|
||||||
_refresh_catalog_scroll_indicators.call_deferred()
|
_refresh_catalog_scroll_indicators.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_index_page() -> void:
|
||||||
|
_set_index_open(not _index_open)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_index_open(open: bool, restore_focus: bool = true) -> void:
|
||||||
|
_index_open = open
|
||||||
|
if _index_tab != null:
|
||||||
|
_index_tab.set_selected(open)
|
||||||
|
_index_tab.tooltip_text = (
|
||||||
|
"Return to catch record" if open else "Open catalog index"
|
||||||
|
)
|
||||||
|
if _index_body != null:
|
||||||
|
_index_body.visible = open
|
||||||
|
if _detail_body != null:
|
||||||
|
_detail_body.visible = not open
|
||||||
|
if not open:
|
||||||
|
_refresh_details(false)
|
||||||
|
_update_index_controls()
|
||||||
|
if restore_focus and _active and _interactive:
|
||||||
|
if open:
|
||||||
|
_controller_zone = ControllerZone.INDEX
|
||||||
|
set_interactive(true)
|
||||||
|
_index_search.grab_focus()
|
||||||
|
elif _controller_zone == ControllerZone.INDEX:
|
||||||
|
_controller_zone = ControllerZone.TABS
|
||||||
|
set_interactive(true)
|
||||||
|
_index_tab.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_index_search_changed(value: String) -> void:
|
||||||
|
if _index_update_in_progress:
|
||||||
|
return
|
||||||
|
_index_options["search"] = value
|
||||||
|
_refresh_catalog()
|
||||||
|
|
||||||
|
|
||||||
|
func _cycle_index_option(key: StringName, direction: int = 1) -> void:
|
||||||
|
if not _index_options.has(key):
|
||||||
|
return
|
||||||
|
if key == &"descending":
|
||||||
|
_index_options[key] = not bool(_index_options[key])
|
||||||
|
elif key == &"group":
|
||||||
|
var groups: Array[String] = [""]
|
||||||
|
if _catalog != null:
|
||||||
|
groups.append_array(LogbookCatalog.available_groups(
|
||||||
|
_catalog.candidates,
|
||||||
|
_category,
|
||||||
|
_collection_log,
|
||||||
|
))
|
||||||
|
var current_index := groups.find(str(_index_options[key]))
|
||||||
|
if current_index < 0:
|
||||||
|
current_index = 0
|
||||||
|
_index_options[key] = groups[wrapi(
|
||||||
|
current_index + direction,
|
||||||
|
0,
|
||||||
|
groups.size(),
|
||||||
|
)]
|
||||||
|
else:
|
||||||
|
var option_count := _index_option_count(key)
|
||||||
|
if option_count <= 0:
|
||||||
|
return
|
||||||
|
_index_options[key] = wrapi(
|
||||||
|
int(_index_options[key]) + direction,
|
||||||
|
0,
|
||||||
|
option_count,
|
||||||
|
)
|
||||||
|
_refresh_catalog()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_index_option_gui_input(
|
||||||
|
event: InputEvent,
|
||||||
|
key: StringName,
|
||||||
|
) -> void:
|
||||||
|
var mouse_event := event as InputEventMouseButton
|
||||||
|
if mouse_event == null or not mouse_event.pressed:
|
||||||
|
return
|
||||||
|
if mouse_event.button_index in [MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_WHEEL_UP]:
|
||||||
|
_cycle_index_option(key, -1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||||
|
_cycle_index_option(key, 1)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _index_key_for_control(control: Control) -> StringName:
|
||||||
|
for key: StringName in _index_option_buttons:
|
||||||
|
if _index_option_buttons[key] == control:
|
||||||
|
return key
|
||||||
|
return StringName()
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_index() -> void:
|
||||||
|
_index_options = {
|
||||||
|
"search": "",
|
||||||
|
"show": LogbookCatalog.DiscoveryFilter.ALL,
|
||||||
|
"sort": LogbookCatalog.SortMode.CATALOG_NUMBER,
|
||||||
|
"descending": false,
|
||||||
|
"availability": LogbookCatalog.AvailabilityFilter.ANY,
|
||||||
|
"season": LogbookCatalog.SeasonFilter.ANY,
|
||||||
|
"time": LogbookCatalog.TimeFilter.ANY,
|
||||||
|
"rarity": LogbookCatalog.RarityFilter.ANY,
|
||||||
|
"method": LogbookCatalog.MethodFilter.ANY,
|
||||||
|
"holding": LogbookCatalog.HoldingFilter.ANY,
|
||||||
|
"group": "",
|
||||||
|
}
|
||||||
|
_index_update_in_progress = true
|
||||||
|
_index_search.text = ""
|
||||||
|
_index_update_in_progress = false
|
||||||
|
_refresh_catalog()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_index_controls(
|
||||||
|
visible_count: int = -1,
|
||||||
|
category_total: int = -1,
|
||||||
|
) -> void:
|
||||||
|
if visible_count >= 0:
|
||||||
|
_index_visible_count = visible_count
|
||||||
|
if category_total >= 0:
|
||||||
|
_index_category_total = category_total
|
||||||
|
if _index_summary != null:
|
||||||
|
_index_summary.text = "%d shown of %d" % [
|
||||||
|
_index_visible_count,
|
||||||
|
_index_category_total,
|
||||||
|
]
|
||||||
|
for key: StringName in _index_option_buttons:
|
||||||
|
var button := _index_option_buttons[key]
|
||||||
|
if button == null:
|
||||||
|
continue
|
||||||
|
button.text = _index_option_label(key)
|
||||||
|
button.accessibility_name = "%s: %s" % [
|
||||||
|
String(key).replace("_", " "),
|
||||||
|
button.text,
|
||||||
|
]
|
||||||
|
if _index_tab != null:
|
||||||
|
var marked := _index_has_active_criteria()
|
||||||
|
_index_tab.text = "index •" if marked else "index"
|
||||||
|
_index_tab.accessibility_name = (
|
||||||
|
"Catalog index, filters active" if marked else "Catalog index"
|
||||||
|
)
|
||||||
|
if _index_reset != null:
|
||||||
|
_index_reset.disabled = not _index_has_active_criteria()
|
||||||
|
|
||||||
|
|
||||||
|
func _index_option_label(key: StringName) -> String:
|
||||||
|
match key:
|
||||||
|
&"show":
|
||||||
|
return INDEX_SHOW_LABELS[int(_index_options[key])]
|
||||||
|
&"sort":
|
||||||
|
return INDEX_SORT_LABELS[int(_index_options[key])]
|
||||||
|
&"availability":
|
||||||
|
return INDEX_AVAILABILITY_LABELS[int(_index_options[key])]
|
||||||
|
&"season":
|
||||||
|
return INDEX_SEASON_LABELS[int(_index_options[key])]
|
||||||
|
&"time":
|
||||||
|
return INDEX_TIME_LABELS[int(_index_options[key])]
|
||||||
|
&"rarity":
|
||||||
|
return INDEX_RARITY_LABELS[int(_index_options[key])]
|
||||||
|
&"method":
|
||||||
|
return INDEX_METHOD_LABELS[int(_index_options[key])]
|
||||||
|
&"holding":
|
||||||
|
return INDEX_HOLDING_LABELS[int(_index_options[key])]
|
||||||
|
&"group":
|
||||||
|
var group := str(_index_options[key])
|
||||||
|
return group if not group.is_empty() else "any group"
|
||||||
|
&"descending":
|
||||||
|
var descending := bool(_index_options[key])
|
||||||
|
match int(_index_options["sort"]):
|
||||||
|
LogbookCatalog.SortMode.NAME:
|
||||||
|
return "z–a" if descending else "a–z"
|
||||||
|
LogbookCatalog.SortMode.CATALOG_NUMBER:
|
||||||
|
return "high–low" if descending else "low–high"
|
||||||
|
LogbookCatalog.SortMode.RARITY:
|
||||||
|
return (
|
||||||
|
"rare–common" if descending else "common–rare"
|
||||||
|
)
|
||||||
|
LogbookCatalog.SortMode.VALUE, LogbookCatalog.SortMode.WEIGHT:
|
||||||
|
return "high–low" if descending else "low–high"
|
||||||
|
_:
|
||||||
|
return "most first" if descending else "least first"
|
||||||
|
return "any"
|
||||||
|
|
||||||
|
|
||||||
|
func _index_option_count(key: StringName) -> int:
|
||||||
|
match key:
|
||||||
|
&"show":
|
||||||
|
return INDEX_SHOW_LABELS.size()
|
||||||
|
&"sort":
|
||||||
|
return INDEX_SORT_LABELS.size()
|
||||||
|
&"availability":
|
||||||
|
return INDEX_AVAILABILITY_LABELS.size()
|
||||||
|
&"season":
|
||||||
|
return INDEX_SEASON_LABELS.size()
|
||||||
|
&"time":
|
||||||
|
return INDEX_TIME_LABELS.size()
|
||||||
|
&"rarity":
|
||||||
|
return INDEX_RARITY_LABELS.size()
|
||||||
|
&"method":
|
||||||
|
return INDEX_METHOD_LABELS.size()
|
||||||
|
&"holding":
|
||||||
|
return INDEX_HOLDING_LABELS.size()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
func _index_has_active_criteria() -> bool:
|
||||||
|
return (
|
||||||
|
not str(_index_options["search"]).is_empty()
|
||||||
|
or int(_index_options["show"])
|
||||||
|
!= LogbookCatalog.DiscoveryFilter.ALL
|
||||||
|
or int(_index_options["sort"])
|
||||||
|
!= LogbookCatalog.SortMode.CATALOG_NUMBER
|
||||||
|
or bool(_index_options["descending"])
|
||||||
|
or int(_index_options["availability"])
|
||||||
|
!= LogbookCatalog.AvailabilityFilter.ANY
|
||||||
|
or int(_index_options["season"])
|
||||||
|
!= LogbookCatalog.SeasonFilter.ANY
|
||||||
|
or int(_index_options["time"])
|
||||||
|
!= LogbookCatalog.TimeFilter.ANY
|
||||||
|
or int(_index_options["rarity"])
|
||||||
|
!= LogbookCatalog.RarityFilter.ANY
|
||||||
|
or int(_index_options["method"])
|
||||||
|
!= LogbookCatalog.MethodFilter.ANY
|
||||||
|
or int(_index_options["holding"])
|
||||||
|
!= LogbookCatalog.HoldingFilter.ANY
|
||||||
|
or not str(_index_options["group"]).is_empty()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _current_season() -> int:
|
||||||
|
return (
|
||||||
|
_world_time.get_season()
|
||||||
|
if _world_time != null
|
||||||
|
else CalendarSeason.UNKNOWN
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _current_is_night() -> bool:
|
||||||
|
return _world_time != null and _world_time.is_night_period()
|
||||||
|
|
||||||
|
|
||||||
|
func notify_availability_context_changed() -> void:
|
||||||
|
_on_logbook_context_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_logbook_context_changed(
|
||||||
|
_first: Variant = null,
|
||||||
|
_second: Variant = null,
|
||||||
|
) -> void:
|
||||||
|
if (
|
||||||
|
int(_index_options["availability"])
|
||||||
|
== LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW
|
||||||
|
or int(_index_options["season"])
|
||||||
|
== LogbookCatalog.SeasonFilter.CURRENT
|
||||||
|
or int(_index_options["time"])
|
||||||
|
== LogbookCatalog.TimeFilter.CURRENT
|
||||||
|
):
|
||||||
|
_refresh_catalog()
|
||||||
|
|
||||||
|
|
||||||
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
|
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
|
||||||
var entry := Button.new()
|
var entry := Button.new()
|
||||||
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
|
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
|
||||||
|
|
@ -663,8 +1283,11 @@ func _add_entry_content(
|
||||||
|
|
||||||
func _select_category(category: LogbookCatalog.Category) -> void:
|
func _select_category(category: LogbookCatalog.Category) -> void:
|
||||||
if category == _category:
|
if category == _category:
|
||||||
|
if _index_open:
|
||||||
|
_set_index_open(false)
|
||||||
return
|
return
|
||||||
_category = category
|
_category = category
|
||||||
|
_set_index_open(false, false)
|
||||||
_selected_id = StringName()
|
_selected_id = StringName()
|
||||||
_selected_entry_key = StringName()
|
_selected_entry_key = StringName()
|
||||||
for index: int in _category_tabs.size():
|
for index: int in _category_tabs.size():
|
||||||
|
|
@ -683,6 +1306,11 @@ func _configure_category_focus() -> void:
|
||||||
tab.focus_neighbor_right = tab.get_path_to(
|
tab.focus_neighbor_right = tab.get_path_to(
|
||||||
_category_tabs[mini(index + 1, _category_tabs.size() - 1)]
|
_category_tabs[mini(index + 1, _category_tabs.size() - 1)]
|
||||||
)
|
)
|
||||||
|
if not _category_tabs.is_empty() and _index_tab != null:
|
||||||
|
var final_tab: Button = _category_tabs.back()
|
||||||
|
final_tab.focus_neighbor_right = final_tab.get_path_to(_index_tab)
|
||||||
|
_index_tab.focus_neighbor_left = _index_tab.get_path_to(final_tab)
|
||||||
|
_index_tab.focus_neighbor_right = _index_tab.get_path_to(_index_tab)
|
||||||
|
|
||||||
|
|
||||||
func _animate_tab_entry() -> void:
|
func _animate_tab_entry() -> void:
|
||||||
|
|
@ -690,11 +1318,15 @@ func _animate_tab_entry() -> void:
|
||||||
(_category_tabs[index] as OrganizerTabType).animate_entrance(
|
(_category_tabs[index] as OrganizerTabType).animate_entrance(
|
||||||
float(index) * 0.025
|
float(index) * 0.025
|
||||||
)
|
)
|
||||||
|
if _index_tab != null:
|
||||||
|
_index_tab.animate_entrance(float(_category_tabs.size()) * 0.025)
|
||||||
|
|
||||||
|
|
||||||
func _settle_tabs_for_close() -> void:
|
func _settle_tabs_for_close() -> void:
|
||||||
for tab: OrganizerTabType in _category_tabs:
|
for tab: OrganizerTabType in _category_tabs:
|
||||||
tab.settle_for_close()
|
tab.settle_for_close()
|
||||||
|
if _index_tab != null:
|
||||||
|
_index_tab.settle_for_close()
|
||||||
|
|
||||||
|
|
||||||
func _begin_category_transition() -> void:
|
func _begin_category_transition() -> void:
|
||||||
|
|
@ -750,9 +1382,12 @@ func _set_category_content_alpha(alpha: float) -> void:
|
||||||
_empty_state.modulate.a = alpha
|
_empty_state.modulate.a = alpha
|
||||||
if _detail_body != null:
|
if _detail_body != null:
|
||||||
_detail_body.modulate.a = alpha
|
_detail_body.modulate.a = alpha
|
||||||
|
if _index_body != null:
|
||||||
|
_index_body.modulate.a = alpha
|
||||||
|
|
||||||
|
|
||||||
func _select_entry(entry_key: StringName, fish_id: StringName) -> void:
|
func _select_entry(entry_key: StringName, fish_id: StringName) -> void:
|
||||||
|
_set_index_open(false, false)
|
||||||
if fish_id.is_empty():
|
if fish_id.is_empty():
|
||||||
_selected_id = StringName()
|
_selected_id = StringName()
|
||||||
_selected_entry_key = entry_key
|
_selected_entry_key = entry_key
|
||||||
|
|
@ -1284,7 +1919,9 @@ func _on_collection_changed() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_inventory_changed() -> void:
|
func _on_inventory_changed() -> void:
|
||||||
_refresh_details(false)
|
# Ownership filters and favorite-backed views depend on the live cooler,
|
||||||
|
# while rebuilding the catalog also keeps the selected detail current.
|
||||||
|
_refresh_catalog()
|
||||||
|
|
||||||
|
|
||||||
func _unknown_selection_key(fish: FishDataType) -> StringName:
|
func _unknown_selection_key(fish: FishDataType) -> StringName:
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ signal controller_hotbar_management_requested(initial_slot: int)
|
||||||
signal controller_hotbar_management_ended
|
signal controller_hotbar_management_ended
|
||||||
signal menu_exit_started
|
signal menu_exit_started
|
||||||
signal shop_cooler_modal_changed(is_open: bool)
|
signal shop_cooler_modal_changed(is_open: bool)
|
||||||
|
signal controller_text_entry_requested(control: Control)
|
||||||
|
|
||||||
enum Section {
|
enum Section {
|
||||||
COOLER,
|
COOLER,
|
||||||
|
|
@ -409,6 +410,9 @@ func _ready() -> void:
|
||||||
_logbook_tab.pressed.connect(
|
_logbook_tab.pressed.connect(
|
||||||
_show_section.bind(Section.LOGBOOK)
|
_show_section.bind(Section.LOGBOOK)
|
||||||
)
|
)
|
||||||
|
_catalog_logbook.controller_text_entry_requested.connect(
|
||||||
|
controller_text_entry_requested.emit
|
||||||
|
)
|
||||||
_the_net_tab.pressed.connect(_show_section.bind(Section.NET))
|
_the_net_tab.pressed.connect(_show_section.bind(Section.NET))
|
||||||
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
|
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
|
||||||
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
|
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
|
||||||
|
|
@ -556,6 +560,7 @@ func setup(
|
||||||
discovery: DiscoveryClient,
|
discovery: DiscoveryClient,
|
||||||
player_jobs: PlayerJobService,
|
player_jobs: PlayerJobService,
|
||||||
world_time: WorldTimeService,
|
world_time: WorldTimeService,
|
||||||
|
world_weather: WorldWeatherService,
|
||||||
world_environment: WorldEnvironment,
|
world_environment: WorldEnvironment,
|
||||||
world_sun: DirectionalLight3D,
|
world_sun: DirectionalLight3D,
|
||||||
) -> void:
|
) -> void:
|
||||||
|
|
@ -589,7 +594,14 @@ func setup(
|
||||||
world_sun,
|
world_sun,
|
||||||
)
|
)
|
||||||
_players_page.setup(network_player_list, discovery)
|
_players_page.setup(network_player_list, discovery)
|
||||||
_catalog_logbook.setup(collection_log, inventory, catalog)
|
_catalog_logbook.setup(
|
||||||
|
collection_log,
|
||||||
|
inventory,
|
||||||
|
catalog,
|
||||||
|
fishing_spot,
|
||||||
|
world_time,
|
||||||
|
world_weather,
|
||||||
|
)
|
||||||
_the_net_page.setup(player_jobs, world_time)
|
_the_net_page.setup(player_jobs, world_time)
|
||||||
_network_mail_service.unread_count_changed.connect(
|
_network_mail_service.unread_count_changed.connect(
|
||||||
_on_mail_unread_count_changed
|
_on_mail_unread_count_changed
|
||||||
|
|
@ -1547,11 +1559,14 @@ func open_menu() -> void:
|
||||||
_cancel_page_tween()
|
_cancel_page_tween()
|
||||||
_prior_movement_enabled = _player.is_movement_enabled()
|
_prior_movement_enabled = _player.is_movement_enabled()
|
||||||
_prior_camera_input_enabled = _player.is_camera_input_enabled()
|
_prior_camera_input_enabled = _player.is_camera_input_enabled()
|
||||||
_prior_mouse_mode = Input.mouse_mode
|
_player.set_movement_enabled(false)
|
||||||
|
# Ending an active right-click camera drag restores its ordinary pointer
|
||||||
|
# mode. Snapshot after that handoff so this menu never restores the drag's
|
||||||
|
# transient captured mode when it closes.
|
||||||
|
_player.set_camera_input_enabled(false)
|
||||||
|
_prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode)
|
||||||
_control_snapshot_stored = true
|
_control_snapshot_stored = true
|
||||||
_mouse_snapshot_stored = true
|
_mouse_snapshot_stored = true
|
||||||
_player.set_movement_enabled(false)
|
|
||||||
_player.set_camera_input_enabled(false)
|
|
||||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
visible = true
|
visible = true
|
||||||
|
|
@ -1763,6 +1778,16 @@ func _apply_mouse_close_policy(reason: CloseReason) -> void:
|
||||||
_mouse_snapshot_stored = false
|
_mouse_snapshot_stored = false
|
||||||
|
|
||||||
|
|
||||||
|
func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
|
||||||
|
# Mouse capture belongs to an in-progress camera drag, never to the player
|
||||||
|
# menu itself. A held right button can be released while this menu owns
|
||||||
|
# input, so restoring CAPTURED here would leave the pointer hidden without
|
||||||
|
# an active drag to release it.
|
||||||
|
if mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
|
return Input.MOUSE_MODE_VISIBLE
|
||||||
|
return mouse_mode
|
||||||
|
|
||||||
|
|
||||||
func _on_bite_activated() -> void:
|
func _on_bite_activated() -> void:
|
||||||
if visible:
|
if visible:
|
||||||
close_menu(CloseReason.BITE_STARTED)
|
close_menu(CloseReason.BITE_STARTED)
|
||||||
|
|
@ -2779,6 +2804,7 @@ func _toggle_active_tackle() -> void:
|
||||||
|
|
||||||
func _on_active_bait_changed(_item_id: StringName) -> void:
|
func _on_active_bait_changed(_item_id: StringName) -> void:
|
||||||
_update_tackle_detail()
|
_update_tackle_detail()
|
||||||
|
_catalog_logbook.notify_availability_context_changed()
|
||||||
|
|
||||||
|
|
||||||
func _on_active_lure_changed(_item_id: StringName) -> void:
|
func _on_active_lure_changed(_item_id: StringName) -> void:
|
||||||
|
|
@ -3733,6 +3759,7 @@ func _refresh_all() -> void:
|
||||||
func _on_bag_changed() -> void:
|
func _on_bag_changed() -> void:
|
||||||
_refresh_bag()
|
_refresh_bag()
|
||||||
_refresh_tackle_box()
|
_refresh_tackle_box()
|
||||||
|
_catalog_logbook.notify_availability_context_changed()
|
||||||
|
|
||||||
|
|
||||||
func _on_hotbar_changed() -> void:
|
func _on_hotbar_changed() -> void:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ extends Control
|
||||||
const INPUT_OWNER: StringName = &"player_storage"
|
const INPUT_OWNER: StringName = &"player_storage"
|
||||||
|
|
||||||
signal menu_visibility_changed(is_open: bool)
|
signal menu_visibility_changed(is_open: bool)
|
||||||
|
signal decorate_requested
|
||||||
|
|
||||||
var _player: Player
|
var _player: Player
|
||||||
var _fishing_spot: FishingSpot
|
var _fishing_spot: FishingSpot
|
||||||
|
|
@ -14,6 +15,9 @@ var _storage_grid: GeneralInventoryGrid
|
||||||
var _inventory_count: Label
|
var _inventory_count: Label
|
||||||
var _storage_count: Label
|
var _storage_count: Label
|
||||||
var _feedback: Label
|
var _feedback: Label
|
||||||
|
var _title: Label
|
||||||
|
var _decorate_button: Button
|
||||||
|
var _home_access: bool = false
|
||||||
var _prior_movement_enabled: bool = true
|
var _prior_movement_enabled: bool = true
|
||||||
var _prior_camera_enabled: bool = true
|
var _prior_camera_enabled: bool = true
|
||||||
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
|
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
@ -75,20 +79,42 @@ func set_world_interaction(interaction: PlayerStorageInteraction) -> void:
|
||||||
|
|
||||||
|
|
||||||
func open_storage() -> bool:
|
func open_storage() -> bool:
|
||||||
|
return _open_storage(false)
|
||||||
|
|
||||||
|
|
||||||
|
func open_storage_from_home(allow_decorating: bool = true) -> bool:
|
||||||
|
return _open_storage(true, allow_decorating)
|
||||||
|
|
||||||
|
|
||||||
|
func _open_storage(
|
||||||
|
home_access: bool,
|
||||||
|
allow_decorating: bool = false,
|
||||||
|
) -> bool:
|
||||||
if (
|
if (
|
||||||
visible
|
visible
|
||||||
or _player == null
|
or _player == null
|
||||||
or _interaction == null
|
|
||||||
or not _interaction.is_local_player_in_range()
|
|
||||||
or _fishing_spot == null
|
or _fishing_spot == null
|
||||||
or not _fishing_spot.can_open_fishing_shop()
|
or not _fishing_spot.can_open_fishing_shop()
|
||||||
|
or (
|
||||||
|
not home_access
|
||||||
|
and (
|
||||||
|
_interaction == null
|
||||||
|
or not _interaction.is_local_player_in_range()
|
||||||
|
)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
|
_home_access = home_access
|
||||||
|
_title.text = "RV storage" if home_access else "private storage"
|
||||||
|
_decorate_button.visible = home_access and allow_decorating
|
||||||
_prior_movement_enabled = _player.is_movement_enabled()
|
_prior_movement_enabled = _player.is_movement_enabled()
|
||||||
_prior_camera_enabled = _player.is_camera_input_enabled()
|
_prior_camera_enabled = _player.is_camera_input_enabled()
|
||||||
_prior_mouse_mode = Input.mouse_mode
|
|
||||||
_player.set_movement_enabled(false)
|
_player.set_movement_enabled(false)
|
||||||
|
# End an in-progress right-click camera drag before taking the snapshot.
|
||||||
|
# A held button can be released while this page owns input, so captured mode
|
||||||
|
# is never a valid mouse mode to restore when the page closes.
|
||||||
_player.set_camera_input_enabled(false)
|
_player.set_camera_input_enabled(false)
|
||||||
|
_prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode)
|
||||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
_feedback.text = "select an item to move it · drag to choose a slot"
|
_feedback.text = "select an item to move it · drag to choose a slot"
|
||||||
|
|
@ -112,10 +138,13 @@ func close_storage(restore_controls: bool = true) -> void:
|
||||||
_player.set_movement_enabled(_prior_movement_enabled)
|
_player.set_movement_enabled(_prior_movement_enabled)
|
||||||
_player.set_camera_input_enabled(_prior_camera_enabled)
|
_player.set_camera_input_enabled(_prior_camera_enabled)
|
||||||
Input.mouse_mode = _prior_mouse_mode
|
Input.mouse_mode = _prior_mouse_mode
|
||||||
|
_home_access = false
|
||||||
menu_visibility_changed.emit(false)
|
menu_visibility_changed.emit(false)
|
||||||
|
|
||||||
|
|
||||||
func close_for_range_exit() -> void:
|
func close_for_range_exit() -> void:
|
||||||
|
if _home_access:
|
||||||
|
return
|
||||||
close_storage()
|
close_storage()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -127,6 +156,12 @@ func close_for_session_end() -> void:
|
||||||
close_storage(false)
|
close_storage(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
|
||||||
|
if mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
|
return Input.MOUSE_MODE_VISIBLE
|
||||||
|
return mouse_mode
|
||||||
|
|
||||||
|
|
||||||
func consume_escape() -> bool:
|
func consume_escape() -> bool:
|
||||||
if not visible:
|
if not visible:
|
||||||
return false
|
return false
|
||||||
|
|
@ -248,12 +283,20 @@ func _build_ui() -> void:
|
||||||
|
|
||||||
var header := HBoxContainer.new()
|
var header := HBoxContainer.new()
|
||||||
layout.add_child(header)
|
layout.add_child(header)
|
||||||
var title := Label.new()
|
_title = Label.new()
|
||||||
title.text = "private storage"
|
_title.text = "private storage"
|
||||||
title.add_theme_font_size_override("font_size", 32)
|
_title.add_theme_font_size_override("font_size", 32)
|
||||||
title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY)
|
_title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY)
|
||||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
header.add_child(title)
|
header.add_child(_title)
|
||||||
|
_decorate_button = Button.new()
|
||||||
|
_decorate_button.text = "decorate RV"
|
||||||
|
_decorate_button.tooltip_text = "Place decor inside your RV"
|
||||||
|
_decorate_button.custom_minimum_size = Vector2(160.0, 48.0)
|
||||||
|
UtilityPageStyle.apply_ocean_button(_decorate_button)
|
||||||
|
_decorate_button.hide()
|
||||||
|
_decorate_button.pressed.connect(_request_decorating)
|
||||||
|
header.add_child(_decorate_button)
|
||||||
var close := Button.new()
|
var close := Button.new()
|
||||||
close.text = "×"
|
close.text = "×"
|
||||||
close.tooltip_text = "Close storage"
|
close.tooltip_text = "Close storage"
|
||||||
|
|
@ -285,6 +328,13 @@ func _build_ui() -> void:
|
||||||
layout.add_child(_feedback)
|
layout.add_child(_feedback)
|
||||||
|
|
||||||
|
|
||||||
|
func _request_decorating() -> void:
|
||||||
|
if not _home_access:
|
||||||
|
return
|
||||||
|
close_storage()
|
||||||
|
decorate_requested.emit()
|
||||||
|
|
||||||
|
|
||||||
func _build_column(parent: HBoxContainer, title_text: String) -> Dictionary:
|
func _build_column(parent: HBoxContainer, title_text: String) -> Dictionary:
|
||||||
var panel := PanelContainer.new()
|
var panel := PanelContainer.new()
|
||||||
panel.custom_minimum_size = Vector2(550.0, 0.0)
|
panel.custom_minimum_size = Vector2(550.0, 0.0)
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
|
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
|
||||||
theme_override_font_sizes/font_size = 22
|
theme_override_font_sizes/font_size = 22
|
||||||
text = "v0.19.0-alpha"
|
text = "v0.20.2-alpha"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"]
|
[node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"]
|
||||||
|
|
|
||||||
2
world/decor_shop_interaction.gd
Normal file
2
world/decor_shop_interaction.gd
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
class_name DecorShopInteraction
|
||||||
|
extends FishingShopInteraction
|
||||||
1
world/decor_shop_interaction.gd.uid
Normal file
1
world/decor_shop_interaction.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://u70jgwedk7ym
|
||||||
|
|
@ -9,6 +9,9 @@ const FishingShopInteractionType = preload(
|
||||||
const PlayerStorageInteractionType = preload(
|
const PlayerStorageInteractionType = preload(
|
||||||
"res://world/player_storage_interaction.gd"
|
"res://world/player_storage_interaction.gd"
|
||||||
)
|
)
|
||||||
|
const DecorShopInteractionType = preload(
|
||||||
|
"res://world/decor_shop_interaction.gd"
|
||||||
|
)
|
||||||
const PrecipitationOcclusionType = preload(
|
const PrecipitationOcclusionType = preload(
|
||||||
"res://world/environment/precipitation_occlusion.gd"
|
"res://world/environment/precipitation_occlusion.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -43,6 +46,12 @@ const PROP_MINIMUM_GROUND_CLEARANCE := 0.05
|
||||||
const PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE := 0.35
|
const PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE := 0.35
|
||||||
const PROP_CHANCE_SCALE := 10000
|
const PROP_CHANCE_SCALE := 10000
|
||||||
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
const PROP_SELECTION_WEIGHT_SCALE := 1000
|
||||||
|
const WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE := 0.05
|
||||||
|
const WATER_RECOVERY_MINIMUM_UP_DOT := 0.6
|
||||||
|
const WATER_RECOVERY_FOOTPRINT_RADIUS := 0.6
|
||||||
|
const WATER_RECOVERY_INSET_STEP := 0.25
|
||||||
|
const WATER_RECOVERY_MAXIMUM_INSET := 1.5
|
||||||
|
const WATER_RECOVERY_SEARCH_EXPANSIONS: Array[float] = [1.5, 4.0, 12.0]
|
||||||
const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [
|
const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [
|
||||||
Vector2(1.0, 0.0),
|
Vector2(1.0, 0.0),
|
||||||
Vector2(0.70710678, 0.70710678),
|
Vector2(0.70710678, 0.70710678),
|
||||||
|
|
@ -74,8 +83,12 @@ const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
|
||||||
@onready var _player_storage: PlayerStorageInteractionType = (
|
@onready var _player_storage: PlayerStorageInteractionType = (
|
||||||
$Interactables/PlayerStorageBox/InteractionArea
|
$Interactables/PlayerStorageBox/InteractionArea
|
||||||
)
|
)
|
||||||
|
@onready var _decor_shop: DecorShopInteractionType = (
|
||||||
|
$Interactables/DecorShopWorld/InteractionArea
|
||||||
|
)
|
||||||
@onready var _shop_root: Node3D = %FishingShopWorld
|
@onready var _shop_root: Node3D = %FishingShopWorld
|
||||||
@onready var _storage_root: Node3D = %PlayerStorageBox
|
@onready var _storage_root: Node3D = %PlayerStorageBox
|
||||||
|
@onready var _decor_shop_root: Node3D = %DecorShopWorld
|
||||||
@onready var _decorations: Node3D = %Decorations
|
@onready var _decorations: Node3D = %Decorations
|
||||||
@onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks
|
@onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks
|
||||||
@onready var _diggable_beach: DiggableArea3D = %DiggableBeach
|
@onready var _diggable_beach: DiggableArea3D = %DiggableBeach
|
||||||
|
|
@ -90,6 +103,7 @@ var _placed_prop_clearance_radii: Array[float] = []
|
||||||
var _placed_prop_groups: Array[StringName] = []
|
var _placed_prop_groups: Array[StringName] = []
|
||||||
var _placed_group_coordinates: Dictionary[StringName, Array] = {}
|
var _placed_group_coordinates: Dictionary[StringName, Array] = {}
|
||||||
var _biome_assignments: Dictionary[Vector2i, StringName] = {}
|
var _biome_assignments: Dictionary[Vector2i, StringName] = {}
|
||||||
|
var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -141,6 +155,10 @@ func get_player_storage() -> PlayerStorageInteractionType:
|
||||||
return _player_storage
|
return _player_storage
|
||||||
|
|
||||||
|
|
||||||
|
func get_decor_shop() -> DecorShopInteractionType:
|
||||||
|
return _decor_shop
|
||||||
|
|
||||||
|
|
||||||
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||||
return _shoreline_reference
|
return _shoreline_reference
|
||||||
|
|
||||||
|
|
@ -190,6 +208,37 @@ func get_spawn_surface_triangles(
|
||||||
return triangles
|
return triangles
|
||||||
|
|
||||||
|
|
||||||
|
func get_water_recovery_position(
|
||||||
|
entry_position: Vector3,
|
||||||
|
fallback_position: Vector3,
|
||||||
|
) -> Vector3:
|
||||||
|
if (
|
||||||
|
not entry_position.is_finite()
|
||||||
|
or _water_recovery_triangles_by_coordinate.is_empty()
|
||||||
|
):
|
||||||
|
return super(entry_position, fallback_position)
|
||||||
|
var nearest_surface_distance := _nearest_recovery_surface_distance(
|
||||||
|
entry_position
|
||||||
|
)
|
||||||
|
if not is_finite(nearest_surface_distance):
|
||||||
|
return super(entry_position, fallback_position)
|
||||||
|
for expansion: float in WATER_RECOVERY_SEARCH_EXPANSIONS:
|
||||||
|
var maximum_distance := nearest_surface_distance + expansion
|
||||||
|
var result: Variant = _nearest_safe_recovery_surface_position(
|
||||||
|
entry_position,
|
||||||
|
maximum_distance,
|
||||||
|
)
|
||||||
|
if result is Vector3:
|
||||||
|
return result as Vector3
|
||||||
|
var map_wide_result: Variant = _nearest_safe_recovery_surface_position(
|
||||||
|
entry_position,
|
||||||
|
get_playable_half_extents().length() * 2.0,
|
||||||
|
)
|
||||||
|
if map_wide_result is Vector3:
|
||||||
|
return map_wide_result as Vector3
|
||||||
|
return super(entry_position, fallback_position)
|
||||||
|
|
||||||
|
|
||||||
func _on_generation_completed(summary: Dictionary) -> void:
|
func _on_generation_completed(summary: Dictionary) -> void:
|
||||||
_diggable_beach.invalidate_surface_cache()
|
_diggable_beach.invalidate_surface_cache()
|
||||||
var records: Array[Dictionary] = _generator.placement_records()
|
var records: Array[Dictionary] = _generator.placement_records()
|
||||||
|
|
@ -325,6 +374,10 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
||||||
grass_surface_triangles_by_coordinate,
|
grass_surface_triangles_by_coordinate,
|
||||||
sand_surface_triangles_by_coordinate,
|
sand_surface_triangles_by_coordinate,
|
||||||
)
|
)
|
||||||
|
_cache_water_recovery_surfaces(
|
||||||
|
grass_surface_triangles_by_coordinate,
|
||||||
|
sand_surface_triangles_by_coordinate,
|
||||||
|
)
|
||||||
_place_spawn_amenities(spawn_position)
|
_place_spawn_amenities(spawn_position)
|
||||||
_configure_fresh_water(records)
|
_configure_fresh_water(records)
|
||||||
_configure_diggable_area()
|
_configure_diggable_area()
|
||||||
|
|
@ -386,6 +439,8 @@ func _place_spawn_amenities(center: Vector3) -> void:
|
||||||
_shop_root.rotation.y = PI
|
_shop_root.rotation.y = PI
|
||||||
_storage_root.position = center + Vector3(-2.4, 0.0, 1.2)
|
_storage_root.position = center + Vector3(-2.4, 0.0, 1.2)
|
||||||
_storage_root.rotation.y = PI * 0.5
|
_storage_root.rotation.y = PI * 0.5
|
||||||
|
_decor_shop_root.position = center + Vector3(-2.4, 0.0, 1.2)
|
||||||
|
_decor_shop_root.rotation.y = PI * 0.5
|
||||||
|
|
||||||
|
|
||||||
func _configure_static_water() -> void:
|
func _configure_static_water() -> void:
|
||||||
|
|
@ -1261,6 +1316,203 @@ func _terrain_surface_triangles_by_coordinate(
|
||||||
return triangles_by_coordinate
|
return triangles_by_coordinate
|
||||||
|
|
||||||
|
|
||||||
|
func _cache_water_recovery_surfaces(
|
||||||
|
grass_triangles: Dictionary[Vector2i, Array],
|
||||||
|
sand_triangles: Dictionary[Vector2i, Array],
|
||||||
|
) -> void:
|
||||||
|
_water_recovery_triangles_by_coordinate.clear()
|
||||||
|
_append_water_recovery_surfaces(grass_triangles)
|
||||||
|
_append_water_recovery_surfaces(sand_triangles)
|
||||||
|
|
||||||
|
|
||||||
|
func _append_water_recovery_surfaces(
|
||||||
|
source: Dictionary[Vector2i, Array],
|
||||||
|
) -> void:
|
||||||
|
for coordinate: Vector2i in source:
|
||||||
|
var eligible: Array = _water_recovery_triangles_by_coordinate.get(
|
||||||
|
coordinate,
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
for triangle: PackedVector3Array in source[coordinate]:
|
||||||
|
if triangle.size() != 3:
|
||||||
|
continue
|
||||||
|
if minf(
|
||||||
|
triangle[0].y,
|
||||||
|
minf(triangle[1].y, triangle[2].y),
|
||||||
|
) <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE:
|
||||||
|
continue
|
||||||
|
var cross := (triangle[1] - triangle[0]).cross(
|
||||||
|
triangle[2] - triangle[0]
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
cross.length_squared() <= 0.0000001
|
||||||
|
or absf(cross.normalized().dot(Vector3.UP))
|
||||||
|
< WATER_RECOVERY_MINIMUM_UP_DOT
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
eligible.append(triangle)
|
||||||
|
if not eligible.is_empty():
|
||||||
|
_water_recovery_triangles_by_coordinate[coordinate] = eligible
|
||||||
|
|
||||||
|
|
||||||
|
func _nearest_recovery_surface_distance(entry_position: Vector3) -> float:
|
||||||
|
var entry_horizontal := Vector2(entry_position.x, entry_position.z)
|
||||||
|
var nearest_distance_squared := INF
|
||||||
|
for coordinate: Vector2i in _water_recovery_triangles_by_coordinate:
|
||||||
|
for triangle: PackedVector3Array in (
|
||||||
|
_water_recovery_triangles_by_coordinate[coordinate]
|
||||||
|
):
|
||||||
|
var closest := _closest_horizontal_point_on_triangle(
|
||||||
|
entry_horizontal,
|
||||||
|
triangle,
|
||||||
|
)
|
||||||
|
nearest_distance_squared = minf(
|
||||||
|
nearest_distance_squared,
|
||||||
|
entry_horizontal.distance_squared_to(closest),
|
||||||
|
)
|
||||||
|
return sqrt(nearest_distance_squared)
|
||||||
|
|
||||||
|
|
||||||
|
func _nearest_safe_recovery_surface_position(
|
||||||
|
entry_position: Vector3,
|
||||||
|
maximum_distance: float,
|
||||||
|
) -> Variant:
|
||||||
|
var entry_horizontal := Vector2(entry_position.x, entry_position.z)
|
||||||
|
var maximum_distance_squared := maximum_distance * maximum_distance
|
||||||
|
var best_distance_squared := INF
|
||||||
|
var best_position: Variant = null
|
||||||
|
for coordinate: Vector2i in _water_recovery_triangles_by_coordinate:
|
||||||
|
for triangle: PackedVector3Array in (
|
||||||
|
_water_recovery_triangles_by_coordinate[coordinate]
|
||||||
|
):
|
||||||
|
var closest := _closest_horizontal_point_on_triangle(
|
||||||
|
entry_horizontal,
|
||||||
|
triangle,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
entry_horizontal.distance_squared_to(closest)
|
||||||
|
> maximum_distance_squared
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var centroid := Vector2(
|
||||||
|
(triangle[0].x + triangle[1].x + triangle[2].x) / 3.0,
|
||||||
|
(triangle[0].z + triangle[1].z + triangle[2].z) / 3.0,
|
||||||
|
)
|
||||||
|
var inset_distance := minf(
|
||||||
|
closest.distance_to(centroid),
|
||||||
|
WATER_RECOVERY_MAXIMUM_INSET,
|
||||||
|
)
|
||||||
|
var inset_steps := ceili(
|
||||||
|
inset_distance / WATER_RECOVERY_INSET_STEP
|
||||||
|
)
|
||||||
|
for inset_index: int in inset_steps + 1:
|
||||||
|
var candidate_horizontal := closest.move_toward(
|
||||||
|
centroid,
|
||||||
|
minf(
|
||||||
|
float(inset_index) * WATER_RECOVERY_INSET_STEP,
|
||||||
|
inset_distance,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
var distance_squared := entry_horizontal.distance_squared_to(
|
||||||
|
candidate_horizontal
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
distance_squared > maximum_distance_squared
|
||||||
|
or distance_squared >= best_distance_squared
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var candidate := Vector3(
|
||||||
|
candidate_horizontal.x,
|
||||||
|
entry_position.y,
|
||||||
|
candidate_horizontal.y,
|
||||||
|
)
|
||||||
|
var safe_position: Variant = _safe_recovery_surface_position(
|
||||||
|
candidate
|
||||||
|
)
|
||||||
|
if safe_position is Vector3:
|
||||||
|
best_distance_squared = distance_squared
|
||||||
|
best_position = safe_position
|
||||||
|
return best_position
|
||||||
|
|
||||||
|
|
||||||
|
func _safe_recovery_surface_position(candidate: Vector3) -> Variant:
|
||||||
|
var nearby_triangles := _water_recovery_surface_triangles_near(candidate)
|
||||||
|
var surface_height := _surface_height_at(candidate, nearby_triangles)
|
||||||
|
if (
|
||||||
|
surface_height <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE
|
||||||
|
or not _surface_supports_prop_footprint(
|
||||||
|
candidate,
|
||||||
|
surface_height,
|
||||||
|
WATER_RECOVERY_FOOTPRINT_RADIUS,
|
||||||
|
nearby_triangles,
|
||||||
|
)
|
||||||
|
or not _has_prop_clearance(
|
||||||
|
candidate,
|
||||||
|
WATER_RECOVERY_FOOTPRINT_RADIUS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
candidate.y = surface_height
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
func _water_recovery_surface_triangles_near(
|
||||||
|
position: Vector3,
|
||||||
|
) -> Array[PackedVector3Array]:
|
||||||
|
var result: Array[PackedVector3Array] = []
|
||||||
|
if _generator.catalog == null or _generator.catalog.chunk_size <= 0.0:
|
||||||
|
return result
|
||||||
|
var local_position := _generator.to_local(position)
|
||||||
|
var half_grid := Vector2(
|
||||||
|
float(_generator.grid_size.x - 1) * 0.5,
|
||||||
|
float(_generator.grid_size.y - 1) * 0.5,
|
||||||
|
)
|
||||||
|
var coordinate := Vector2i(
|
||||||
|
roundi(local_position.x / _generator.catalog.chunk_size + half_grid.x),
|
||||||
|
roundi(local_position.z / _generator.catalog.chunk_size + half_grid.y),
|
||||||
|
)
|
||||||
|
for offset_x: int in range(-1, 2):
|
||||||
|
for offset_y: int in range(-1, 2):
|
||||||
|
var nearby_coordinate := coordinate + Vector2i(offset_x, offset_y)
|
||||||
|
result.append_array(
|
||||||
|
_water_recovery_triangles_by_coordinate.get(
|
||||||
|
nearby_coordinate,
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _closest_horizontal_point_on_triangle(
|
||||||
|
point: Vector2,
|
||||||
|
triangle: PackedVector3Array,
|
||||||
|
) -> Vector2:
|
||||||
|
var polygon := PackedVector2Array([
|
||||||
|
Vector2(triangle[0].x, triangle[0].z),
|
||||||
|
Vector2(triangle[1].x, triangle[1].z),
|
||||||
|
Vector2(triangle[2].x, triangle[2].z),
|
||||||
|
])
|
||||||
|
if Geometry2D.is_point_in_polygon(point, polygon):
|
||||||
|
return point
|
||||||
|
var closest := Geometry2D.get_closest_point_to_segment(
|
||||||
|
point,
|
||||||
|
polygon[0],
|
||||||
|
polygon[1],
|
||||||
|
)
|
||||||
|
var nearest_distance_squared := point.distance_squared_to(closest)
|
||||||
|
for edge_index: int in range(1, 3):
|
||||||
|
var edge_closest := Geometry2D.get_closest_point_to_segment(
|
||||||
|
point,
|
||||||
|
polygon[edge_index],
|
||||||
|
polygon[(edge_index + 1) % 3],
|
||||||
|
)
|
||||||
|
var distance_squared := point.distance_squared_to(edge_closest)
|
||||||
|
if distance_squared < nearest_distance_squared:
|
||||||
|
nearest_distance_squared = distance_squared
|
||||||
|
closest = edge_closest
|
||||||
|
return closest
|
||||||
|
|
||||||
|
|
||||||
func _terrain_coordinate_for(mesh_instance: MeshInstance3D) -> Vector2i:
|
func _terrain_coordinate_for(mesh_instance: MeshInstance3D) -> Vector2i:
|
||||||
var current: Node = mesh_instance
|
var current: Node = mesh_instance
|
||||||
while current != null and current != _generator:
|
while current != null and current != _generator:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=14 format=3]
|
[gd_scene load_steps=15 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"]
|
[ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"]
|
||||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
|
[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
[ext_resource type="Resource" path="res://fish/pools/starter_pond_pool.tres" id="11_pond_pool"]
|
[ext_resource type="Resource" path="res://fish/pools/starter_pond_pool.tres" id="11_pond_pool"]
|
||||||
[ext_resource type="Resource" path="res://world/generation/props/terrain_prop_catalog.tres" id="12_prop_catalog"]
|
[ext_resource type="Resource" path="res://world/generation/props/terrain_prop_catalog.tres" id="12_prop_catalog"]
|
||||||
[ext_resource type="Resource" path="res://world/generation/biomes/terrain_biome_catalog.tres" id="13_biome_catalog"]
|
[ext_resource type="Resource" path="res://world/generation/biomes/terrain_biome_catalog.tres" id="13_biome_catalog"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/interactables/decor_shop_world.tscn" id="14_decor_shop"]
|
||||||
|
|
||||||
[node name="GeneratedWorldRegion" type="Node3D"]
|
[node name="GeneratedWorldRegion" type="Node3D"]
|
||||||
script = ExtResource("1_region")
|
script = ExtResource("1_region")
|
||||||
|
|
@ -98,6 +99,9 @@ unique_name_in_owner = true
|
||||||
[node name="PlayerStorageBox" parent="Interactables" instance=ExtResource("6_storage")]
|
[node name="PlayerStorageBox" parent="Interactables" instance=ExtResource("6_storage")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
[node name="DecorShopWorld" parent="Interactables" instance=ExtResource("14_decor_shop")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
|
||||||
[node name="Decorations" type="Node3D" parent="."]
|
[node name="Decorations" type="Node3D" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
|
|
||||||
35
world/interactables/decor_shop_world.tscn
Normal file
35
world/interactables/decor_shop_world.tscn
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
[gd_scene load_steps=5 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/decor_shop_interaction.gd" id="1_shop"]
|
||||||
|
[ext_resource type="PackedScene" path="res://art/exported/characters/base/straywild_base_character.glb" id="2_character"]
|
||||||
|
[ext_resource type="Script" path="res://world/world_character_display.gd" id="3_display"]
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="InteractionShape"]
|
||||||
|
resource_local_to_scene = true
|
||||||
|
radius = 3.0
|
||||||
|
|
||||||
|
[node name="DecorShopWorld" type="Node3D"]
|
||||||
|
|
||||||
|
[node name="Shopkeeper" parent="." instance=ExtResource("2_character")]
|
||||||
|
script = ExtResource("3_display")
|
||||||
|
species_id = "round"
|
||||||
|
fur_color_id = "orange"
|
||||||
|
ears_id = "pointy_short"
|
||||||
|
tail_id = "cat"
|
||||||
|
eyes_id = "simple_shine"
|
||||||
|
nose_id = "dog_round"
|
||||||
|
mouth_id = "three"
|
||||||
|
head_look_enabled = true
|
||||||
|
|
||||||
|
[node name="AnimationPlayer" parent="Shopkeeper"]
|
||||||
|
autoplay = "idle"
|
||||||
|
|
||||||
|
[node name="InteractionArea" type="Area3D" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
position = Vector3(0, 1, -0.3)
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 2
|
||||||
|
script = ExtResource("1_shop")
|
||||||
|
|
||||||
|
[node name="InteractionShape" type="CollisionShape3D" parent="InteractionArea"]
|
||||||
|
shape = SubResource("InteractionShape")
|
||||||
|
|
@ -27,6 +27,8 @@ size = Vector3(1.62, 0.86, 1.02)
|
||||||
radius = 2.3
|
radius = 2.3
|
||||||
|
|
||||||
[node name="PlayerStorageBox" type="Node3D"]
|
[node name="PlayerStorageBox" type="Node3D"]
|
||||||
|
process_mode = 4
|
||||||
|
visible = false
|
||||||
|
|
||||||
[node name="Body" type="MeshInstance3D" parent="."]
|
[node name="Body" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(0, 0.35, 0)
|
position = Vector3(0, 0.35, 0)
|
||||||
|
|
@ -44,6 +46,7 @@ collision_mask = 0
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
||||||
position = Vector3(0, 0.43, 0)
|
position = Vector3(0, 0.43, 0)
|
||||||
|
disabled = true
|
||||||
shape = SubResource("CollisionShape")
|
shape = SubResource("CollisionShape")
|
||||||
|
|
||||||
[node name="InteractionArea" type="Area3D" parent="."]
|
[node name="InteractionArea" type="Area3D" parent="."]
|
||||||
|
|
@ -54,6 +57,7 @@ collision_mask = 2
|
||||||
script = ExtResource("1_interaction")
|
script = ExtResource("1_interaction")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="InteractionArea"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="InteractionArea"]
|
||||||
|
disabled = true
|
||||||
shape = SubResource("InteractionShape")
|
shape = SubResource("InteractionShape")
|
||||||
|
|
||||||
[node name="PromptAnchor" type="Marker3D" parent="."]
|
[node name="PromptAnchor" type="Marker3D" parent="."]
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ const FishingShopInteractionType = preload(
|
||||||
const PlayerStorageInteractionType = preload(
|
const PlayerStorageInteractionType = preload(
|
||||||
"res://world/player_storage_interaction.gd"
|
"res://world/player_storage_interaction.gd"
|
||||||
)
|
)
|
||||||
|
const DecorShopInteractionType = preload(
|
||||||
|
"res://world/decor_shop_interaction.gd"
|
||||||
|
)
|
||||||
const FOLIAGE_WIND_SHADER: Shader = preload(
|
const FOLIAGE_WIND_SHADER: Shader = preload(
|
||||||
"res://world/materials/foliage_wind.gdshader"
|
"res://world/materials/foliage_wind.gdshader"
|
||||||
)
|
)
|
||||||
|
|
@ -49,6 +52,10 @@ var fishing_shop_path: NodePath = (
|
||||||
var player_storage_path: NodePath = (
|
var player_storage_path: NodePath = (
|
||||||
^"Interactables/PlayerStorageBox/InteractionArea"
|
^"Interactables/PlayerStorageBox/InteractionArea"
|
||||||
)
|
)
|
||||||
|
@export_node_path("Area3D")
|
||||||
|
var decor_shop_path: NodePath = (
|
||||||
|
^"Interactables/DecorShopWorld/InteractionArea"
|
||||||
|
)
|
||||||
@export_group("Terrain Collision")
|
@export_group("Terrain Collision")
|
||||||
# The imported GLB is the collision authority. Rebuild once per region load so
|
# The imported GLB is the collision authority. Rebuild once per region load so
|
||||||
# newly authored terrain and props cannot retain a stale saved fallback shape.
|
# newly authored terrain and props cannot retain a stale saved fallback shape.
|
||||||
|
|
@ -84,6 +91,10 @@ func get_player_storage() -> PlayerStorageInteractionType:
|
||||||
return get_node_or_null(player_storage_path) as PlayerStorageInteractionType
|
return get_node_or_null(player_storage_path) as PlayerStorageInteractionType
|
||||||
|
|
||||||
|
|
||||||
|
func get_decor_shop() -> DecorShopInteractionType:
|
||||||
|
return get_node_or_null(decor_shop_path) as DecorShopInteractionType
|
||||||
|
|
||||||
|
|
||||||
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||||
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
|
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -18,6 +18,10 @@ func get_fishing_shop() -> FishingShopInteraction:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_decor_shop() -> DecorShopInteraction:
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
func get_player_storage() -> PlayerStorageInteraction:
|
func get_player_storage() -> PlayerStorageInteraction:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
@ -72,6 +76,24 @@ func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
|
||||||
return points
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
func get_water_recovery_position(
|
||||||
|
entry_position: Vector3,
|
||||||
|
fallback_position: Vector3,
|
||||||
|
) -> Vector3:
|
||||||
|
if not entry_position.is_finite():
|
||||||
|
return fallback_position
|
||||||
|
var target_position := fallback_position
|
||||||
|
var nearest_distance := INF
|
||||||
|
for point: SafeRespawnPoint in get_safe_respawn_points():
|
||||||
|
if point == null or not is_instance_valid(point) or not point.enabled:
|
||||||
|
continue
|
||||||
|
var distance := point.get_horizontal_distance_squared(entry_position)
|
||||||
|
if distance < nearest_distance:
|
||||||
|
nearest_distance = distance
|
||||||
|
target_position = point.global_position
|
||||||
|
return target_position
|
||||||
|
|
||||||
|
|
||||||
func get_diggable_areas() -> Array[DiggableArea3D]:
|
func get_diggable_areas() -> Array[DiggableArea3D]:
|
||||||
var areas: Array[DiggableArea3D] = []
|
var areas: Array[DiggableArea3D] = []
|
||||||
var root: Node = get_node_or_null(diggable_area_root)
|
var root: Node = get_node_or_null(diggable_area_root)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ const FishingShopInteractionType = preload(
|
||||||
const PlayerStorageInteractionType = preload(
|
const PlayerStorageInteractionType = preload(
|
||||||
"res://world/player_storage_interaction.gd"
|
"res://world/player_storage_interaction.gd"
|
||||||
)
|
)
|
||||||
|
const DecorShopInteractionType = preload(
|
||||||
|
"res://world/decor_shop_interaction.gd"
|
||||||
|
)
|
||||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||||
const GENERATED_REGION_SCENE: PackedScene = preload(
|
const GENERATED_REGION_SCENE: PackedScene = preload(
|
||||||
"res://world/generation/generated_world_region.tscn"
|
"res://world/generation/generated_world_region.tscn"
|
||||||
|
|
@ -31,6 +34,7 @@ var _world_layout: StringName = WorldLayoutType.GENERATED
|
||||||
var _world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED
|
var _world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||||
var _light_performance_profile: bool = false
|
var _light_performance_profile: bool = false
|
||||||
var _dedicated_simulation: bool = false
|
var _dedicated_simulation: bool = false
|
||||||
|
var _local_home_interior_presentation_active: bool = false
|
||||||
|
|
||||||
|
|
||||||
func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
||||||
|
|
@ -49,6 +53,16 @@ func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_water_recovery_position(entry_position: Vector3) -> Vector3:
|
||||||
|
if _active_region == null:
|
||||||
|
return global_position
|
||||||
|
var fallback_position := get_player_spawn_transform().origin
|
||||||
|
return _active_region.get_water_recovery_position(
|
||||||
|
entry_position,
|
||||||
|
fallback_position,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func get_fishing_shop() -> FishingShopInteractionType:
|
func get_fishing_shop() -> FishingShopInteractionType:
|
||||||
return _active_region.get_fishing_shop()
|
return _active_region.get_fishing_shop()
|
||||||
|
|
||||||
|
|
@ -57,6 +71,10 @@ func get_player_storage() -> PlayerStorageInteractionType:
|
||||||
return _active_region.get_player_storage()
|
return _active_region.get_player_storage()
|
||||||
|
|
||||||
|
|
||||||
|
func get_decor_shop() -> DecorShopInteractionType:
|
||||||
|
return _active_region.get_decor_shop()
|
||||||
|
|
||||||
|
|
||||||
func get_player_spawn_transform() -> Transform3D:
|
func get_player_spawn_transform() -> Transform3D:
|
||||||
return _active_region.get_player_spawn_transform()
|
return _active_region.get_player_spawn_transform()
|
||||||
|
|
||||||
|
|
@ -75,6 +93,12 @@ func set_light_performance_profile(enabled: bool) -> void:
|
||||||
_active_region.set_light_performance_profile(enabled)
|
_active_region.set_light_performance_profile(enabled)
|
||||||
|
|
||||||
|
|
||||||
|
func set_local_home_interior_presentation_active(enabled: bool) -> void:
|
||||||
|
_local_home_interior_presentation_active = enabled
|
||||||
|
if _active_region != null and not _dedicated_simulation:
|
||||||
|
_active_region.visible = not enabled
|
||||||
|
|
||||||
|
|
||||||
func set_dedicated_simulation(enabled: bool) -> void:
|
func set_dedicated_simulation(enabled: bool) -> void:
|
||||||
_dedicated_simulation = enabled
|
_dedicated_simulation = enabled
|
||||||
if _active_region != null:
|
if _active_region != null:
|
||||||
|
|
@ -262,6 +286,8 @@ func _replace_active_region(layout: StringName, seed: int) -> bool:
|
||||||
_active_region.set_light_performance_profile(_light_performance_profile)
|
_active_region.set_light_performance_profile(_light_performance_profile)
|
||||||
if _dedicated_simulation:
|
if _dedicated_simulation:
|
||||||
_set_region_presentation_enabled(_active_region, false)
|
_set_region_presentation_enabled(_active_region, false)
|
||||||
|
else:
|
||||||
|
_active_region.visible = not _local_home_interior_presentation_active
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ var _game_ui: GameUI
|
||||||
var _screen_fade: ScreenFade
|
var _screen_fade: ScreenFade
|
||||||
var _water_triggers: Array[PlayerWaterTrigger] = []
|
var _water_triggers: Array[PlayerWaterTrigger] = []
|
||||||
var _safe_points: Array[SafeRespawnPoint] = []
|
var _safe_points: Array[SafeRespawnPoint] = []
|
||||||
|
var _recovery_position_resolver := Callable()
|
||||||
var _initial_spawn_transform: Transform3D
|
var _initial_spawn_transform: Transform3D
|
||||||
var _entry_position: Vector3
|
var _entry_position: Vector3
|
||||||
var _bob_base_position: Vector3
|
var _bob_base_position: Vector3
|
||||||
|
|
@ -43,6 +44,7 @@ func setup(
|
||||||
screen_fade: ScreenFade,
|
screen_fade: ScreenFade,
|
||||||
water_triggers: Array[PlayerWaterTrigger],
|
water_triggers: Array[PlayerWaterTrigger],
|
||||||
safe_points: Array[SafeRespawnPoint],
|
safe_points: Array[SafeRespawnPoint],
|
||||||
|
recovery_position_resolver: Callable = Callable(),
|
||||||
) -> void:
|
) -> void:
|
||||||
_player = player
|
_player = player
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
|
|
@ -50,6 +52,7 @@ func setup(
|
||||||
_screen_fade = screen_fade
|
_screen_fade = screen_fade
|
||||||
_water_triggers = water_triggers
|
_water_triggers = water_triggers
|
||||||
_safe_points = safe_points
|
_safe_points = safe_points
|
||||||
|
_recovery_position_resolver = recovery_position_resolver
|
||||||
_initial_spawn_transform = player.global_transform
|
_initial_spawn_transform = player.global_transform
|
||||||
for water_trigger: PlayerWaterTrigger in _water_triggers:
|
for water_trigger: PlayerWaterTrigger in _water_triggers:
|
||||||
if (
|
if (
|
||||||
|
|
@ -73,6 +76,7 @@ func update_world_context(
|
||||||
initial_spawn_transform: Transform3D,
|
initial_spawn_transform: Transform3D,
|
||||||
water_triggers: Array[PlayerWaterTrigger],
|
water_triggers: Array[PlayerWaterTrigger],
|
||||||
safe_points: Array[SafeRespawnPoint],
|
safe_points: Array[SafeRespawnPoint],
|
||||||
|
recovery_position_resolver: Callable = Callable(),
|
||||||
) -> void:
|
) -> void:
|
||||||
for water_trigger: PlayerWaterTrigger in _water_triggers:
|
for water_trigger: PlayerWaterTrigger in _water_triggers:
|
||||||
if (
|
if (
|
||||||
|
|
@ -88,6 +92,7 @@ func update_world_context(
|
||||||
_initial_spawn_transform = initial_spawn_transform
|
_initial_spawn_transform = initial_spawn_transform
|
||||||
_water_triggers = water_triggers
|
_water_triggers = water_triggers
|
||||||
_safe_points = safe_points
|
_safe_points = safe_points
|
||||||
|
_recovery_position_resolver = recovery_position_resolver
|
||||||
for water_trigger: PlayerWaterTrigger in _water_triggers:
|
for water_trigger: PlayerWaterTrigger in _water_triggers:
|
||||||
if (
|
if (
|
||||||
water_trigger != null
|
water_trigger != null
|
||||||
|
|
@ -183,17 +188,7 @@ func _on_fade_transition_completed(
|
||||||
|
|
||||||
|
|
||||||
func _respawn_player() -> void:
|
func _respawn_player() -> void:
|
||||||
var target_position: Vector3 = _initial_spawn_transform.origin
|
var target_position := _resolve_recovery_position()
|
||||||
var nearest_distance: float = INF
|
|
||||||
for point: SafeRespawnPoint in _safe_points:
|
|
||||||
if point == null or not is_instance_valid(point) or not point.enabled:
|
|
||||||
continue
|
|
||||||
var distance: float = point.get_horizontal_distance_squared(
|
|
||||||
_entry_position
|
|
||||||
)
|
|
||||||
if distance < nearest_distance:
|
|
||||||
nearest_distance = distance
|
|
||||||
target_position = point.global_position
|
|
||||||
target_position.y += respawn_height_offset
|
target_position.y += respawn_height_offset
|
||||||
var respawn_transform: Transform3D = _player.global_transform
|
var respawn_transform: Transform3D = _player.global_transform
|
||||||
respawn_transform.basis = _recovery_root_basis
|
respawn_transform.basis = _recovery_root_basis
|
||||||
|
|
@ -204,6 +199,23 @@ func _respawn_player() -> void:
|
||||||
local_respawn_completed.emit(_entry_position)
|
local_respawn_completed.emit(_entry_position)
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_recovery_position() -> Vector3:
|
||||||
|
if _recovery_position_resolver.is_valid():
|
||||||
|
var resolved: Variant = _recovery_position_resolver.call(_entry_position)
|
||||||
|
if resolved is Vector3 and (resolved as Vector3).is_finite():
|
||||||
|
return resolved as Vector3
|
||||||
|
var target_position := _initial_spawn_transform.origin
|
||||||
|
var nearest_distance := INF
|
||||||
|
for point: SafeRespawnPoint in _safe_points:
|
||||||
|
if point == null or not is_instance_valid(point) or not point.enabled:
|
||||||
|
continue
|
||||||
|
var distance := point.get_horizontal_distance_squared(_entry_position)
|
||||||
|
if distance < nearest_distance:
|
||||||
|
nearest_distance = distance
|
||||||
|
target_position = point.global_position
|
||||||
|
return target_position
|
||||||
|
|
||||||
|
|
||||||
func _finish_recovery() -> void:
|
func _finish_recovery() -> void:
|
||||||
_player.restore_gameplay_orientation_after_recovery()
|
_player.restore_gameplay_orientation_after_recovery()
|
||||||
_player.set_water_recovery_active(false)
|
_player.set_water_recovery_active(false)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue