Add selectable startup world layouts
This commit is contained in:
parent
5d55afcc26
commit
3d34ed0bee
24 changed files with 1139 additions and 78 deletions
117
main/main.gd
117
main/main.gd
|
|
@ -6,6 +6,7 @@ const FishPoolType = preload("res://fish/fish_pool.gd")
|
|||
const GameUIType = preload("res://ui/game_ui.gd")
|
||||
const PlayerType = preload("res://player/player.gd")
|
||||
const TestWorldType = preload("res://world/test_world.gd")
|
||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||
const WaterRecoveryControllerType = preload(
|
||||
"res://world/water_recovery_controller.gd"
|
||||
)
|
||||
|
|
@ -421,10 +422,17 @@ func _start_dedicated_server() -> void:
|
|||
"The dedicated-server chat privacy configuration is invalid."
|
||||
)
|
||||
return
|
||||
if not _apply_generated_world(config.world_seed, false):
|
||||
if not _apply_world(
|
||||
WorldLayoutType.GENERATED,
|
||||
config.world_seed,
|
||||
false,
|
||||
):
|
||||
_fail_dedicated_server("The configured world seed could not be generated.")
|
||||
return
|
||||
if not _network_session.set_host_world_seed(config.world_seed):
|
||||
if not _network_session.set_host_world(
|
||||
WorldLayoutType.GENERATED,
|
||||
config.world_seed,
|
||||
):
|
||||
_fail_dedicated_server("The configured world seed is invalid.")
|
||||
return
|
||||
_configure_portable_stores()
|
||||
|
|
@ -1693,20 +1701,23 @@ func _resize_native_overlays() -> void:
|
|||
_shop_backdrop.size = Vector2(get_window().size)
|
||||
|
||||
|
||||
func _on_new_game_requested(world_seed: int) -> void:
|
||||
func _on_new_game_requested(
|
||||
world_layout: StringName,
|
||||
world_seed: int,
|
||||
) -> void:
|
||||
if _gameplay_started or _quit_in_progress:
|
||||
return
|
||||
_start_new_game_music()
|
||||
if not _apply_generated_world(world_seed, true):
|
||||
if not _apply_world(world_layout, world_seed, true):
|
||||
_show_title_music(true)
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"Could not generate that world seed. Try rolling another world."
|
||||
"Could not prepare that world. Try another seed or the starter island."
|
||||
)
|
||||
return
|
||||
if not _prepare_host_world_seed(world_seed):
|
||||
if not _prepare_host_world(world_layout, world_seed):
|
||||
_show_title_music(true)
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"Could not prepare the generated world for hosting."
|
||||
"Could not prepare that world for hosting."
|
||||
)
|
||||
return
|
||||
if not _prepare_private_host():
|
||||
|
|
@ -1714,7 +1725,7 @@ func _on_new_game_requested(world_seed: int) -> void:
|
|||
return
|
||||
if (
|
||||
not _save_manager.delete_progression_save()
|
||||
or not _save_manager.initialize_new_game(world_seed)
|
||||
or not _save_manager.initialize_new_game(world_seed, world_layout)
|
||||
):
|
||||
_network_session.disconnect_session("New Game setup failed.")
|
||||
_show_title_music(true)
|
||||
|
|
@ -1733,13 +1744,14 @@ func _on_continue_game_requested() -> void:
|
|||
"Failed to load save. The original was preserved."
|
||||
)
|
||||
return
|
||||
var world_layout: StringName = _save_manager.get_world_layout()
|
||||
var world_seed: int = _save_manager.get_world_seed()
|
||||
if (
|
||||
not _apply_generated_world(world_seed, true)
|
||||
or not _prepare_host_world_seed(world_seed)
|
||||
not _apply_world(world_layout, world_seed, true)
|
||||
or not _prepare_host_world(world_layout, world_seed)
|
||||
):
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"The saved world could not be generated. The save was preserved."
|
||||
"The saved world could not be loaded. The save was preserved."
|
||||
)
|
||||
return
|
||||
if not _prepare_private_host():
|
||||
|
|
@ -1766,13 +1778,16 @@ func _prepare_private_host() -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _prepare_host_world_seed(world_seed: int) -> bool:
|
||||
func _prepare_host_world(
|
||||
world_layout: StringName,
|
||||
world_seed: int,
|
||||
) -> bool:
|
||||
if _network_session.state in [
|
||||
NetworkSessionType.State.CONNECTION_FAILED,
|
||||
NetworkSessionType.State.SERVER_LOST,
|
||||
]:
|
||||
_network_session.reset_failure()
|
||||
return _network_session.set_host_world_seed(world_seed)
|
||||
return _network_session.set_host_world(world_layout, world_seed)
|
||||
|
||||
|
||||
func _enter_gameplay() -> void:
|
||||
|
|
@ -1976,11 +1991,14 @@ func _on_reset_progress_requested() -> void:
|
|||
pause_menu.report_reset_failure()
|
||||
return
|
||||
var world_seed: int = PlayerSaveManager.roll_world_seed()
|
||||
if not _save_manager.initialize_new_game(world_seed):
|
||||
if not _save_manager.initialize_new_game(
|
||||
world_seed,
|
||||
WorldLayoutType.GENERATED,
|
||||
):
|
||||
pause_menu.report_reset_failure()
|
||||
return
|
||||
_network_session.disconnect_session("Progress reset.")
|
||||
if not _apply_generated_world(world_seed, true):
|
||||
if not _apply_world(WorldLayoutType.GENERATED, world_seed, true):
|
||||
pause_menu.report_reset_failure()
|
||||
return
|
||||
pause_menu.close_for_title_transition()
|
||||
|
|
@ -1990,8 +2008,18 @@ func _on_reset_progress_requested() -> void:
|
|||
|
||||
|
||||
func _apply_joined_world(server_metadata: Dictionary) -> bool:
|
||||
var world_layout: StringName = WorldLayoutType.normalized(
|
||||
server_metadata.get(
|
||||
"world_layout",
|
||||
String(WorldLayoutType.GENERATED),
|
||||
),
|
||||
&"",
|
||||
)
|
||||
var world_seed: int = int(server_metadata.get("world_seed", 0))
|
||||
if _apply_generated_world(world_seed, true):
|
||||
if (
|
||||
not world_layout.is_empty()
|
||||
and _apply_world(world_layout, world_seed, true)
|
||||
):
|
||||
return true
|
||||
_network_session.disconnect_session("World generation failed.")
|
||||
_join_requested_from_title = false
|
||||
|
|
@ -2000,20 +2028,26 @@ func _apply_joined_world(server_metadata: Dictionary) -> bool:
|
|||
var title_screen: TitleScreenType = _game_ui.get_title_screen()
|
||||
title_screen.reopen_to_menu()
|
||||
title_screen.report_network_error(
|
||||
"The host's generated world could not be built locally."
|
||||
"The host's world could not be built locally."
|
||||
)
|
||||
_show_title_music(true)
|
||||
return false
|
||||
|
||||
|
||||
func _apply_generated_world(world_seed: int, reposition_player: bool) -> bool:
|
||||
if not _test_world.generate_world(world_seed):
|
||||
func _apply_world(
|
||||
world_layout: StringName,
|
||||
world_seed: int,
|
||||
reposition_player: bool,
|
||||
) -> bool:
|
||||
if not _test_world.activate_world(world_layout, world_seed):
|
||||
return false
|
||||
var spawn_transform: Transform3D = _test_world.get_player_spawn_transform()
|
||||
_player_spawn_service.set_spawn_transform(spawn_transform)
|
||||
if reposition_player:
|
||||
_player.global_transform = spawn_transform
|
||||
_player.velocity = Vector3.ZERO
|
||||
if _application_initialized:
|
||||
_refresh_active_world_bindings()
|
||||
if _application_initialized and not _dedicated_runtime:
|
||||
_water_recovery.update_world_context(
|
||||
spawn_transform,
|
||||
|
|
@ -2027,6 +2061,51 @@ func _apply_generated_world(world_seed: int, reposition_player: bool) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _refresh_active_world_bindings() -> void:
|
||||
if is_instance_valid(_shop_interaction):
|
||||
if _shop_interaction.local_player_range_changed.is_connected(
|
||||
_on_shop_range_changed
|
||||
):
|
||||
_shop_interaction.local_player_range_changed.disconnect(
|
||||
_on_shop_range_changed
|
||||
)
|
||||
if is_instance_valid(_storage_interaction):
|
||||
if _storage_interaction.local_player_range_changed.is_connected(
|
||||
_on_storage_range_changed
|
||||
):
|
||||
_storage_interaction.local_player_range_changed.disconnect(
|
||||
_on_storage_range_changed
|
||||
)
|
||||
_shop_interaction = _test_world.get_fishing_shop()
|
||||
_storage_interaction = _test_world.get_player_storage()
|
||||
_network_sale.set_shop_interaction(_shop_interaction)
|
||||
_network_shop.set_shop_interaction(_shop_interaction)
|
||||
if _dedicated_runtime:
|
||||
return
|
||||
if _shop_interaction != null:
|
||||
_shop_interaction.setup_local_player(_player)
|
||||
_shop_interaction.local_player_range_changed.connect(
|
||||
_on_shop_range_changed
|
||||
)
|
||||
if _storage_interaction != null:
|
||||
_storage_interaction.setup_local_player(_player)
|
||||
_storage_interaction.local_player_range_changed.connect(
|
||||
_on_storage_range_changed
|
||||
)
|
||||
_game_ui.set_world_interactions(
|
||||
_shop_interaction,
|
||||
_storage_interaction,
|
||||
)
|
||||
_on_shop_range_changed(
|
||||
_shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
_on_storage_range_changed(
|
||||
_storage_interaction != null
|
||||
and _storage_interaction.is_local_player_in_range()
|
||||
)
|
||||
|
||||
|
||||
func _on_water_recovery_starting() -> void:
|
||||
_local_recovery_attempt_id = (
|
||||
"recovery:%s"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
class_name NetworkProtocol
|
||||
extends RefCounted
|
||||
|
||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||
|
||||
const PROTOCOL_VERSION: int = 9
|
||||
const GAME_BUILD: String = "prealpha"
|
||||
const MAX_GAME_VERSION_LENGTH: int = 64
|
||||
|
|
@ -29,6 +31,7 @@ const JOBS_CAPABILITY: String = "jobs_v1"
|
|||
const WORLD_SPAWN_CAPABILITY: String = "world_spawn_envelope_v1"
|
||||
const APPEARANCE_PREVIEW_CAPABILITY: String = "appearance_preview_v1"
|
||||
const WORLD_GENERATION_CAPABILITY: String = "world_generation_v1"
|
||||
const WORLD_LAYOUT_CAPABILITY: String = "world_layout_v1"
|
||||
const DEFAULT_WORLD_SEED: int = 13001
|
||||
const MAX_WORLD_SEED: int = 2147483646
|
||||
|
||||
|
|
@ -182,6 +185,7 @@ static func make_client_hello(
|
|||
APPEARANCE_PREVIEW_CAPABILITY,
|
||||
BACKPACK_SHOP_CAPABILITY,
|
||||
WORLD_GENERATION_CAPABILITY,
|
||||
WORLD_LAYOUT_CAPABILITY,
|
||||
]),
|
||||
"cosmetic_snapshot": cosmetic_snapshot,
|
||||
"identity_fingerprint": identity_fingerprint,
|
||||
|
|
@ -286,6 +290,7 @@ static func make_server_hello(
|
|||
max_players: int,
|
||||
server_display_name: String = "NETfishing",
|
||||
world_seed: int = DEFAULT_WORLD_SEED,
|
||||
world_layout: StringName = WorldLayoutType.GENERATED,
|
||||
) -> Dictionary:
|
||||
return {
|
||||
"accepted": accepted,
|
||||
|
|
@ -298,6 +303,7 @@ static func make_server_hello(
|
|||
"player_count": player_count,
|
||||
"max_players": max_players,
|
||||
"world_seed": world_seed,
|
||||
"world_layout": String(world_layout),
|
||||
"capability_flags": PackedStringArray([
|
||||
"movement_v1",
|
||||
"fishing_v1",
|
||||
|
|
@ -316,6 +322,7 @@ static func make_server_hello(
|
|||
WORLD_SPAWN_CAPABILITY,
|
||||
APPEARANCE_PREVIEW_CAPABILITY,
|
||||
WORLD_GENERATION_CAPABILITY,
|
||||
WORLD_LAYOUT_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ func setup(
|
|||
_session.state_changed.connect(_on_session_state_changed)
|
||||
|
||||
|
||||
func set_shop_interaction(interaction: FishingShopInteraction) -> void:
|
||||
_shop_interaction = interaction
|
||||
|
||||
|
||||
func is_local_sale_pending() -> bool:
|
||||
return not _pending_local_request_id.is_empty()
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ var _last_server_player_count: int = 0
|
|||
var _last_server_display_name: String = ""
|
||||
var _last_server_protocol_version: int = 0
|
||||
var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
|
||||
var _last_server_world_layout: StringName = WorldLayout.GENERATED
|
||||
var _server_capabilities: PackedStringArray = PackedStringArray()
|
||||
var _profile_ready: bool = false
|
||||
var _player_identity: PlayerIdentityStore
|
||||
|
|
@ -113,6 +114,7 @@ var _session_operator_fingerprints: Dictionary[String, bool] = {}
|
|||
var _operator_peer_ids: Dictionary[int, bool] = {}
|
||||
var _local_operator: bool = false
|
||||
var _host_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
|
||||
var _host_world_layout: StringName = WorldLayout.GENERATED
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -275,6 +277,7 @@ func _register_player_host() -> void:
|
|||
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
||||
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||
|
|
@ -531,20 +534,35 @@ func get_last_server_metadata() -> Dictionary:
|
|||
"player_count": _last_server_player_count,
|
||||
"max_players": _last_server_max_players,
|
||||
"world_seed": _last_server_world_seed,
|
||||
"world_layout": String(_last_server_world_layout),
|
||||
}
|
||||
|
||||
|
||||
func set_host_world_seed(seed: int) -> bool:
|
||||
return set_host_world(WorldLayout.GENERATED, seed)
|
||||
|
||||
|
||||
func set_host_world(world_layout: StringName, seed: int) -> bool:
|
||||
if (
|
||||
state != State.INACTIVE
|
||||
or not WorldLayout.is_valid(world_layout)
|
||||
or seed <= 0
|
||||
or seed > NetworkProtocol.MAX_WORLD_SEED
|
||||
):
|
||||
return false
|
||||
_host_world_layout = world_layout
|
||||
_host_world_seed = seed
|
||||
return true
|
||||
|
||||
|
||||
func get_authoritative_world_layout() -> StringName:
|
||||
return (
|
||||
_last_server_world_layout
|
||||
if is_joined_client()
|
||||
else _host_world_layout
|
||||
)
|
||||
|
||||
|
||||
func get_authoritative_world_seed() -> int:
|
||||
return _last_server_world_seed if is_joined_client() else _host_world_seed
|
||||
|
||||
|
|
@ -583,6 +601,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
||||
NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
|
||||
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
@ -1190,6 +1209,15 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
|
||||
)
|
||||
return
|
||||
if (
|
||||
_host_world_layout == WorldLayout.STARTER_ISLAND
|
||||
and NetworkProtocol.WORLD_LAYOUT_CAPABILITY not in client_capabilities
|
||||
):
|
||||
_reject_peer(
|
||||
sender_id,
|
||||
NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
|
||||
)
|
||||
return
|
||||
var identity: Dictionary = _authenticated_identity_cache.get(sender_id, {})
|
||||
if identity.is_empty():
|
||||
_reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
|
||||
|
|
@ -1278,6 +1306,7 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
session_max_players,
|
||||
_session_display_name,
|
||||
_host_world_seed,
|
||||
_host_world_layout,
|
||||
)
|
||||
)
|
||||
receive_spawn_list.rpc_id(sender_id, _build_spawn_list())
|
||||
|
|
@ -1332,6 +1361,9 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
or typeof(data.get("game_version")) != TYPE_STRING
|
||||
or typeof(data.get("rejection_code")) != TYPE_INT
|
||||
or typeof(data.get("world_seed")) != TYPE_INT
|
||||
or typeof(
|
||||
data.get("world_layout", String(WorldLayout.GENERATED))
|
||||
) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
):
|
||||
_teardown_peer()
|
||||
_fail("The server sent an invalid handshake response.")
|
||||
|
|
@ -1362,6 +1394,14 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
_teardown_peer()
|
||||
_fail("The server sent an invalid world seed.")
|
||||
return
|
||||
var received_world_layout := WorldLayout.normalized(
|
||||
data.get("world_layout", String(WorldLayout.GENERATED)),
|
||||
&"",
|
||||
)
|
||||
if received_world_layout.is_empty():
|
||||
_teardown_peer()
|
||||
_fail("The server sent an invalid world layout.")
|
||||
return
|
||||
_session_id = str(data.get("session_id", ""))
|
||||
_last_server_max_players = int(data.get(
|
||||
"max_players",
|
||||
|
|
@ -1373,6 +1413,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
"protocol_version", NetworkProtocol.PROTOCOL_VERSION
|
||||
))
|
||||
_last_server_world_seed = received_world_seed
|
||||
_last_server_world_layout = received_world_layout
|
||||
_server_capabilities = PackedStringArray()
|
||||
var advertised_capabilities: Variant = data.get(
|
||||
"capability_flags", PackedStringArray()
|
||||
|
|
@ -1391,6 +1432,13 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
_teardown_peer()
|
||||
_fail("This server does not support generated worlds.")
|
||||
return
|
||||
if (
|
||||
received_world_layout == WorldLayout.STARTER_ISLAND
|
||||
and NetworkProtocol.WORLD_LAYOUT_CAPABILITY not in _server_capabilities
|
||||
):
|
||||
_teardown_peer()
|
||||
_fail("This server does not support selectable world layouts.")
|
||||
return
|
||||
var local_peer_id: int = multiplayer.get_unique_id()
|
||||
_registry.clear()
|
||||
_registry.add_peer(
|
||||
|
|
@ -1407,6 +1455,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||
NetworkProtocol.WORLD_GENERATION_CAPABILITY,
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@ func setup(
|
|||
_session.state_changed.connect(_on_session_state_changed)
|
||||
|
||||
|
||||
func set_shop_interaction(interaction: FishingShopInteraction) -> void:
|
||||
_interaction = interaction
|
||||
|
||||
|
||||
func can_request_purchase() -> bool:
|
||||
return (
|
||||
_session != null
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
|||
const WorldWeatherServiceType = preload(
|
||||
"res://world/world_weather_service.gd"
|
||||
)
|
||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
|
||||
const PlayerType = preload("res://player/player.gd")
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ class LoadSnapshot:
|
|||
var cooler_capacity_level: int = 0
|
||||
var art_unlock_mask: int = 0
|
||||
var total_experience: int = 0
|
||||
var world_layout: StringName = WorldLayoutType.GENERATED
|
||||
var world_seed: int = DEFAULT_WORLD_SEED
|
||||
var world_time_hours: float = WorldTimeServiceType.DEFAULT_START_HOUR
|
||||
var has_world_weather_state: bool = false
|
||||
|
|
@ -102,6 +104,7 @@ var _autosave_enabled: bool = false
|
|||
var _save_path := ""
|
||||
var _expected_hash := ""
|
||||
var _data_root: PlayerDataRoot
|
||||
var _world_layout: StringName = WorldLayoutType.GENERATED
|
||||
var _world_seed: int = DEFAULT_WORLD_SEED
|
||||
|
||||
|
||||
|
|
@ -332,6 +335,7 @@ func load_player_data() -> bool:
|
|||
var world_time_restored: bool = (
|
||||
_world_time.restore_persistent_time_hours(snapshot.world_time_hours)
|
||||
)
|
||||
_world_layout = snapshot.world_layout
|
||||
_world_seed = snapshot.world_seed
|
||||
var world_weather_restored: bool = true
|
||||
if snapshot.has_world_weather_state:
|
||||
|
|
@ -521,18 +525,30 @@ func import_progression_archive(path: String) -> Dictionary:
|
|||
}
|
||||
|
||||
|
||||
func initialize_new_game(world_seed: int = DEFAULT_WORLD_SEED) -> bool:
|
||||
func initialize_new_game(
|
||||
world_seed: int = DEFAULT_WORLD_SEED,
|
||||
world_layout: StringName = WorldLayoutType.GENERATED,
|
||||
) -> bool:
|
||||
if not _is_configured:
|
||||
return false
|
||||
if world_seed <= 0 or world_seed > MAX_WORLD_SEED:
|
||||
if (
|
||||
world_seed <= 0
|
||||
or world_seed > MAX_WORLD_SEED
|
||||
or not WorldLayoutType.is_valid(world_layout)
|
||||
):
|
||||
return false
|
||||
_automatic_saving_blocked = false
|
||||
_restore_defaults()
|
||||
_world_layout = world_layout
|
||||
_world_seed = world_seed
|
||||
_is_dirty = true
|
||||
return true
|
||||
|
||||
|
||||
func get_world_layout() -> StringName:
|
||||
return _world_layout
|
||||
|
||||
|
||||
func get_world_seed() -> int:
|
||||
return _world_seed
|
||||
|
||||
|
|
@ -733,6 +749,7 @@ func _build_save_dictionary() -> Dictionary:
|
|||
"art": _art_unlocks.to_save_data(),
|
||||
"experience": _experience.to_save_data(),
|
||||
"world": {
|
||||
"layout": String(_world_layout),
|
||||
"seed": _world_seed,
|
||||
"time_hours": _world_time.get_persistent_time_hours(),
|
||||
"weather": int(_world_weather.get_persistent_weather()),
|
||||
|
|
@ -822,6 +839,12 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
return null
|
||||
|
||||
var snapshot := LoadSnapshot.new()
|
||||
if world_data.has("layout"):
|
||||
if not WorldLayoutType.is_valid(world_data["layout"]):
|
||||
return null
|
||||
snapshot.world_layout = WorldLayoutType.normalized(
|
||||
world_data["layout"]
|
||||
)
|
||||
if world_data.has("seed"):
|
||||
snapshot.world_seed = _read_integer(
|
||||
world_data["seed"],
|
||||
|
|
@ -1525,6 +1548,7 @@ func _prepare_external_save_data(value: Variant) -> Dictionary:
|
|||
"catch_count": snapshot.catches.size(),
|
||||
"wallet_balance": snapshot.wallet_balance,
|
||||
"discovered_species_count": snapshot.discovered_ids.size(),
|
||||
"world_layout": String(snapshot.world_layout),
|
||||
"world_seed": snapshot.world_seed,
|
||||
}
|
||||
|
||||
|
|
@ -1657,6 +1681,7 @@ func _restore_defaults() -> void:
|
|||
WorldTimeServiceType.DEFAULT_START_HOUR
|
||||
)
|
||||
_world_weather.reset_persistent_state()
|
||||
_world_layout = WorldLayoutType.GENERATED
|
||||
_world_seed = DEFAULT_WORLD_SEED
|
||||
_jobs.reset_to_defaults()
|
||||
_is_restoring = false
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ readonly -a QUICK_TESTS=(
|
|||
"tests/tree_gathering_prototype_validation.gd"
|
||||
"tests/unified_inventory_validation.gd"
|
||||
"tests/world_time_validation.gd"
|
||||
"tests/world_layout_validation.gd"
|
||||
"tests/world_spawn_protocol_validation.gd"
|
||||
"tests/world_weather_validation.gd"
|
||||
)
|
||||
|
|
@ -88,6 +89,7 @@ readonly -a NETWORK_TESTS=(
|
|||
"tests/profile_multiplayer_validation.gd"
|
||||
"tests/surface_drawing_multiplayer_validation.gd"
|
||||
"tests/world_time_multiplayer_validation.gd"
|
||||
"tests/world_layout_multiplayer_validation.gd"
|
||||
"tests/world_spawn_multiplayer_validation.gd"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ func _run() -> void:
|
|||
var player := main.get("_player") as Player
|
||||
assert(save_manager != null and player != null)
|
||||
const TEST_WORLD_SEED := 918273
|
||||
assert(save_manager.initialize_new_game(TEST_WORLD_SEED))
|
||||
assert(save_manager.initialize_new_game(
|
||||
TEST_WORLD_SEED,
|
||||
WorldLayout.STARTER_ISLAND,
|
||||
))
|
||||
save_manager.set_autosave_enabled(true)
|
||||
assert(player.bag.add_item(&"art_kit"))
|
||||
assert(player.bag.add_item(&"coffee"))
|
||||
|
|
@ -44,6 +47,7 @@ func _run() -> void:
|
|||
assert(bool(decoded.get("ok", false)))
|
||||
var parsed: Dictionary = decoded["data"]
|
||||
var records: Array = parsed["bag"]["items"]
|
||||
assert(str(parsed["world"]["layout"]) == String(WorldLayout.STARTER_ISLAND))
|
||||
assert(int(parsed["world"]["seed"]) == TEST_WORLD_SEED)
|
||||
assert(_saved_slot(records, &"basic_fishing_rod") == 14)
|
||||
assert(_saved_slot(records, &"coffee") == 12)
|
||||
|
|
@ -53,6 +57,7 @@ func _run() -> void:
|
|||
player.unequip_bait()
|
||||
player.unequip_lure()
|
||||
assert(save_manager.load_player_data())
|
||||
assert(save_manager.get_world_layout() == WorldLayout.STARTER_ISLAND)
|
||||
assert(save_manager.get_world_seed() == TEST_WORLD_SEED)
|
||||
assert(player.bag.get_storage_slot(&"basic_fishing_rod") == 14)
|
||||
assert(player.bag.get_storage_slot(&"coffee") == 12)
|
||||
|
|
|
|||
126
tests/world_layout_multiplayer_validation.gd
Normal file
126
tests/world_layout_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
const TEST_PORT: int = 18155
|
||||
const TEST_SEED: int = 86421357
|
||||
|
||||
|
||||
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("World layout multiplayer 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 save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
assert(bool(main.call(
|
||||
"_apply_world",
|
||||
WorldLayout.STARTER_ISLAND,
|
||||
TEST_SEED,
|
||||
true,
|
||||
)))
|
||||
assert(bool(main.call(
|
||||
"_prepare_host_world",
|
||||
WorldLayout.STARTER_ISLAND,
|
||||
TEST_SEED,
|
||||
)))
|
||||
assert(save_manager.initialize_new_game(
|
||||
TEST_SEED,
|
||||
WorldLayout.STARTER_ISLAND,
|
||||
))
|
||||
assert(session.start_private_host(TEST_PORT))
|
||||
main.call("_enter_gameplay")
|
||||
assert(session.set_host_open(true))
|
||||
var remote_peer_id: int = await _wait_for_remote_peer(session)
|
||||
assert(remote_peer_id > 1)
|
||||
assert(session.peer_supports_capability(
|
||||
remote_peer_id,
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
|
||||
))
|
||||
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("World layout 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 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())
|
||||
assert(session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY
|
||||
))
|
||||
var metadata: Dictionary = session.get_last_server_metadata()
|
||||
assert(str(metadata.get("world_layout", "")) == String(
|
||||
WorldLayout.STARTER_ISLAND
|
||||
))
|
||||
assert(int(metadata.get("world_seed", 0)) == TEST_SEED)
|
||||
var world := main.get("_test_world") as TestWorld
|
||||
assert(world.get_world_layout() == WorldLayout.STARTER_ISLAND)
|
||||
assert(world.get_generation_seed() == TEST_SEED)
|
||||
assert(world.get_node_or_null("Regions/StarterIslandRegion") != null)
|
||||
assert(world.get_fishing_shop() != null)
|
||||
assert(world.get_player_storage() != null)
|
||||
print("World layout 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_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 _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/world_layout_multiplayer_validation.gd.uid
Normal file
1
tests/world_layout_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bg52pbgv5ykjc
|
||||
125
tests/world_layout_validation.gd
Normal file
125
tests/world_layout_validation.gd
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
extends SceneTree
|
||||
|
||||
const TestWorldScene: PackedScene = preload("res://world/test_world.tscn")
|
||||
const SetupPageScene: PackedScene = preload(
|
||||
"res://ui/new_game_setup_page.tscn"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
_validate_layout_values()
|
||||
await _validate_setup_page()
|
||||
await _validate_world_switching()
|
||||
_validate_network_metadata()
|
||||
print("World layout validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_layout_values() -> void:
|
||||
assert(WorldLayout.is_valid(WorldLayout.GENERATED))
|
||||
assert(WorldLayout.is_valid(String(WorldLayout.STARTER_ISLAND)))
|
||||
assert(not WorldLayout.is_valid(&"unknown"))
|
||||
assert(WorldLayout.normalized("unknown") == WorldLayout.GENERATED)
|
||||
assert(WorldLayout.display_name(WorldLayout.STARTER_ISLAND) == "starter island")
|
||||
|
||||
|
||||
func _validate_setup_page() -> void:
|
||||
assert(NewGameSetupPage.parse_seed_text(" 24680 ") == 24680)
|
||||
assert(NewGameSetupPage.parse_seed_text("0") == 0)
|
||||
assert(NewGameSetupPage.parse_seed_text("2147483647") == 0)
|
||||
assert(NewGameSetupPage.parse_seed_text("") == 0)
|
||||
var text_seed: int = NewGameSetupPage.parse_seed_text("voyager island")
|
||||
assert(text_seed > 0 and text_seed <= PlayerSaveManager.MAX_WORLD_SEED)
|
||||
assert(NewGameSetupPage.parse_seed_text(" voyager island ") == text_seed)
|
||||
assert(NewGameSetupPage.parse_seed_text("voyager island") == text_seed)
|
||||
assert(NewGameSetupPage.parse_seed_text("Voyager Island") != text_seed)
|
||||
var page := SetupPageScene.instantiate() as NewGameSetupPage
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
page.open_page(true)
|
||||
await process_frame
|
||||
assert(page.visible)
|
||||
var paper := page.get_node("%Paper") as PanelContainer
|
||||
assert(paper.get_combined_minimum_size().x <= paper.size.x)
|
||||
assert(paper.get_combined_minimum_size().y <= paper.size.y)
|
||||
assert(page.get_selected_world_layout() == WorldLayout.GENERATED)
|
||||
assert(page.get_selected_world_seed() > 0)
|
||||
assert((page.get_node("%OverwriteWarning") as Label).visible)
|
||||
var generated_button := page.get_node("%GeneratedButton") as Button
|
||||
var starter_button := page.get_node("%StarterButton") as Button
|
||||
var random_button := page.get_node("%RandomSeedButton") as Button
|
||||
var custom_button := page.get_node("%CustomSeedButton") as Button
|
||||
var seed_edit := page.get_node("%SeedEdit") as LineEdit
|
||||
var start_button := page.get_node("%StartButton") as Button
|
||||
var back_button := page.get_node("%BackButton") as Button
|
||||
assert(generated_button.get_node(generated_button.focus_neighbor_right) == starter_button)
|
||||
assert(generated_button.get_node(generated_button.focus_neighbor_bottom) == random_button)
|
||||
assert(random_button.get_node(random_button.focus_neighbor_right) == custom_button)
|
||||
assert(start_button.get_node(start_button.focus_neighbor_right) == back_button)
|
||||
page.call("_choose_custom_seed")
|
||||
assert(seed_edit.editable)
|
||||
assert(custom_button.get_node(custom_button.focus_neighbor_bottom) == seed_edit)
|
||||
assert(seed_edit.get_node(seed_edit.focus_neighbor_bottom) == start_button)
|
||||
page.call("_set_world_layout", WorldLayout.STARTER_ISLAND)
|
||||
assert(not (page.get_node("%SeedSection") as Control).visible)
|
||||
var request: Dictionary = {}
|
||||
page.start_requested.connect(func(layout: StringName, seed: int) -> void:
|
||||
request["layout"] = layout
|
||||
request["seed"] = seed
|
||||
)
|
||||
page.call("_request_start")
|
||||
assert(request.get("layout") == WorldLayout.STARTER_ISLAND)
|
||||
assert(int(request.get("seed", 0)) > 0)
|
||||
page.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_world_switching() -> void:
|
||||
var world := TestWorldScene.instantiate() as TestWorld
|
||||
root.add_child(world)
|
||||
await process_frame
|
||||
assert(world.get_world_layout() == WorldLayout.GENERATED)
|
||||
assert(world.get_fishing_shop() != null)
|
||||
assert(world.get_player_storage() != null)
|
||||
assert(world.activate_world(WorldLayout.STARTER_ISLAND, 13579))
|
||||
assert(world.get_world_layout() == WorldLayout.STARTER_ISLAND)
|
||||
assert(world.get_generation_seed() == 13579)
|
||||
assert(world.get_node_or_null("Regions/StarterIslandRegion") != null)
|
||||
assert(world.get_node_or_null("Regions/GeneratedWorldRegion") == null)
|
||||
assert(world.get_fishing_shop() != null)
|
||||
assert(world.get_player_storage() != null)
|
||||
assert(not world.get_fishable_water_regions().is_empty())
|
||||
assert(world.activate_world(WorldLayout.GENERATED, 24680))
|
||||
assert(world.get_world_layout() == WorldLayout.GENERATED)
|
||||
assert(world.get_generation_seed() == 24680)
|
||||
assert(world.get_node_or_null("Regions/GeneratedWorldRegion") != null)
|
||||
assert(world.get_node_or_null("Regions/StarterIslandRegion") == null)
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _validate_network_metadata() -> void:
|
||||
var hello: Dictionary = NetworkProtocol.make_server_hello(
|
||||
true,
|
||||
NetworkProtocol.RejectionCode.NONE,
|
||||
"session",
|
||||
2,
|
||||
1,
|
||||
16,
|
||||
"room",
|
||||
97531,
|
||||
WorldLayout.STARTER_ISLAND,
|
||||
)
|
||||
assert(str(hello.get("world_layout", "")) == String(
|
||||
WorldLayout.STARTER_ISLAND
|
||||
))
|
||||
assert(int(hello.get("world_seed", 0)) == 97531)
|
||||
assert(
|
||||
NetworkProtocol.WORLD_LAYOUT_CAPABILITY
|
||||
in PackedStringArray(hello.get("capability_flags", []))
|
||||
)
|
||||
1
tests/world_layout_validation.gd.uid
Normal file
1
tests/world_layout_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bgxgyqq73sdpi
|
||||
|
|
@ -627,6 +627,10 @@ func setup(
|
|||
_refresh_all()
|
||||
|
||||
|
||||
func set_world_interaction(interaction: ShopInteractionType) -> void:
|
||||
_interaction = interaction
|
||||
|
||||
|
||||
func open_shop() -> bool:
|
||||
if (
|
||||
visible
|
||||
|
|
|
|||
|
|
@ -488,6 +488,16 @@ func setup(
|
|||
)
|
||||
|
||||
|
||||
func set_world_interactions(
|
||||
shop_interaction: ShopInteractionType,
|
||||
storage_interaction: PlayerStorageInteractionType,
|
||||
) -> void:
|
||||
_shop_interaction = shop_interaction
|
||||
_storage_interaction = storage_interaction
|
||||
_fishing_shop.set_world_interaction(shop_interaction)
|
||||
_player_storage.set_world_interaction(storage_interaction)
|
||||
|
||||
|
||||
func _prioritize_surface_drawing_pointer_input() -> void:
|
||||
# Control input follows sibling order rather than CanvasItem.z_index. Chat is
|
||||
# a full-screen Control with interactive mobile children, so the toolbar must
|
||||
|
|
|
|||
309
ui/new_game_setup_page.gd
Normal file
309
ui/new_game_setup_page.gd
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
class_name NewGameSetupPage
|
||||
extends Control
|
||||
|
||||
signal start_requested(world_layout: StringName, world_seed: int)
|
||||
signal back_requested
|
||||
|
||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||
const SaveManagerType = preload("res://save/player_save_manager.gd")
|
||||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
|
||||
enum SeedMode {
|
||||
RANDOM,
|
||||
CUSTOM,
|
||||
}
|
||||
|
||||
@onready var _paper: PanelContainer = %Paper
|
||||
@onready var _generated_button: Button = %GeneratedButton
|
||||
@onready var _starter_button: Button = %StarterButton
|
||||
@onready var _world_description: Label = %WorldDescription
|
||||
@onready var _seed_section: VBoxContainer = %SeedSection
|
||||
@onready var _random_seed_button: Button = %RandomSeedButton
|
||||
@onready var _custom_seed_button: Button = %CustomSeedButton
|
||||
@onready var _seed_edit: LineEdit = %SeedEdit
|
||||
@onready var _seed_help: Label = %SeedHelp
|
||||
@onready var _overwrite_warning: Label = %OverwriteWarning
|
||||
@onready var _status: Label = %Status
|
||||
@onready var _start_button: Button = %StartButton
|
||||
@onready var _back_button: Button = %BackButton
|
||||
|
||||
var _world_layout: StringName = WorldLayoutType.GENERATED
|
||||
var _seed_mode: SeedMode = SeedMode.RANDOM
|
||||
var _random_seed: int = SaveManagerType.DEFAULT_WORLD_SEED
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
UtilityPageStyle.apply_page(self)
|
||||
_paper.add_theme_stylebox_override(
|
||||
"panel", UtilityPageStyle.panel_style()
|
||||
)
|
||||
for button: Button in [
|
||||
_generated_button,
|
||||
_starter_button,
|
||||
_random_seed_button,
|
||||
_custom_seed_button,
|
||||
_start_button,
|
||||
_back_button,
|
||||
]:
|
||||
UtilityPageStyle.apply_ocean_button(button)
|
||||
UtilityPageStyle.apply_ocean_line_edit(_seed_edit)
|
||||
_generated_button.pressed.connect(
|
||||
_set_world_layout.bind(WorldLayoutType.GENERATED)
|
||||
)
|
||||
_starter_button.pressed.connect(
|
||||
_set_world_layout.bind(WorldLayoutType.STARTER_ISLAND)
|
||||
)
|
||||
_random_seed_button.pressed.connect(_choose_random_seed)
|
||||
_custom_seed_button.pressed.connect(_choose_custom_seed)
|
||||
_seed_edit.text_changed.connect(_on_seed_text_changed)
|
||||
_seed_edit.text_submitted.connect(_on_seed_submitted)
|
||||
_start_button.pressed.connect(_request_start)
|
||||
_back_button.pressed.connect(back_requested.emit)
|
||||
hide()
|
||||
|
||||
|
||||
func open_page(has_existing_progression: bool) -> void:
|
||||
_world_layout = WorldLayoutType.GENERATED
|
||||
_seed_mode = SeedMode.RANDOM
|
||||
_roll_random_seed()
|
||||
_overwrite_warning.visible = has_existing_progression
|
||||
_status.text = ""
|
||||
_refresh_presentation()
|
||||
show()
|
||||
UtilityPageStyle.animate_in(self)
|
||||
_generated_button.grab_focus.call_deferred()
|
||||
|
||||
|
||||
func close_page() -> void:
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner != null and is_ancestor_of(focus_owner):
|
||||
focus_owner.release_focus()
|
||||
hide()
|
||||
|
||||
|
||||
func request_back() -> void:
|
||||
back_requested.emit()
|
||||
|
||||
|
||||
func get_selected_world_layout() -> StringName:
|
||||
return _world_layout
|
||||
|
||||
|
||||
func get_selected_world_seed() -> int:
|
||||
return (
|
||||
_random_seed
|
||||
if _seed_mode == SeedMode.RANDOM
|
||||
else parse_seed_text(_seed_edit.text)
|
||||
)
|
||||
|
||||
|
||||
static func parse_seed_text(text: String) -> int:
|
||||
var normalized: String = text.strip_edges()
|
||||
if normalized.is_empty():
|
||||
return 0
|
||||
if normalized.is_valid_int():
|
||||
var numeric_seed: int = int(normalized)
|
||||
if (
|
||||
numeric_seed <= 0
|
||||
or numeric_seed > SaveManagerType.MAX_WORLD_SEED
|
||||
):
|
||||
return 0
|
||||
return numeric_seed
|
||||
var digest: PackedByteArray = normalized.sha256_buffer()
|
||||
var hashed_seed: int = (
|
||||
(int(digest[0]) << 24)
|
||||
| (int(digest[1]) << 16)
|
||||
| (int(digest[2]) << 8)
|
||||
| int(digest[3])
|
||||
) & 0x7fffffff
|
||||
return (hashed_seed % SaveManagerType.MAX_WORLD_SEED) + 1
|
||||
|
||||
|
||||
func _set_world_layout(layout: StringName) -> void:
|
||||
if not WorldLayoutType.is_valid(layout):
|
||||
return
|
||||
_world_layout = layout
|
||||
_status.text = ""
|
||||
_refresh_presentation()
|
||||
if layout == WorldLayoutType.GENERATED:
|
||||
_random_seed_button.grab_focus.call_deferred()
|
||||
else:
|
||||
_start_button.grab_focus.call_deferred()
|
||||
|
||||
|
||||
func _choose_random_seed() -> void:
|
||||
_seed_mode = SeedMode.RANDOM
|
||||
_roll_random_seed()
|
||||
_status.text = ""
|
||||
_refresh_presentation()
|
||||
|
||||
|
||||
func _choose_custom_seed() -> void:
|
||||
_seed_mode = SeedMode.CUSTOM
|
||||
if parse_seed_text(_seed_edit.text) == _random_seed:
|
||||
_seed_edit.text = ""
|
||||
_status.text = ""
|
||||
_refresh_presentation()
|
||||
_seed_edit.grab_focus.call_deferred()
|
||||
_seed_edit.select_all.call_deferred()
|
||||
|
||||
|
||||
func _roll_random_seed() -> void:
|
||||
_random_seed = SaveManagerType.roll_world_seed()
|
||||
_seed_edit.text = str(_random_seed)
|
||||
|
||||
|
||||
func _on_seed_text_changed(_text: String) -> void:
|
||||
if _seed_mode != SeedMode.CUSTOM:
|
||||
return
|
||||
_status.text = ""
|
||||
_refresh_start_state()
|
||||
|
||||
|
||||
func _on_seed_submitted(_text: String) -> void:
|
||||
_request_start()
|
||||
|
||||
|
||||
func _request_start() -> void:
|
||||
var seed: int = get_selected_world_seed()
|
||||
if _world_layout == WorldLayoutType.GENERATED and seed == 0:
|
||||
_status.text = (
|
||||
"enter some text or a whole number from 1 to %d."
|
||||
% SaveManagerType.MAX_WORLD_SEED
|
||||
)
|
||||
_seed_edit.grab_focus()
|
||||
_seed_edit.select_all()
|
||||
return
|
||||
if seed == 0:
|
||||
seed = SaveManagerType.roll_world_seed()
|
||||
_start_button.disabled = true
|
||||
start_requested.emit(_world_layout, seed)
|
||||
|
||||
|
||||
func _refresh_presentation() -> void:
|
||||
var generated: bool = _world_layout == WorldLayoutType.GENERATED
|
||||
_generated_button.set_pressed_no_signal(generated)
|
||||
_starter_button.set_pressed_no_signal(not generated)
|
||||
_world_description.text = (
|
||||
"build a new island from terrain chunks. the same seed always "
|
||||
+ "builds the same world."
|
||||
if generated
|
||||
else "play on the authored starter island."
|
||||
)
|
||||
_seed_section.visible = generated
|
||||
_random_seed_button.set_pressed_no_signal(
|
||||
_seed_mode == SeedMode.RANDOM
|
||||
)
|
||||
_custom_seed_button.set_pressed_no_signal(
|
||||
_seed_mode == SeedMode.CUSTOM
|
||||
)
|
||||
_seed_edit.editable = _seed_mode == SeedMode.CUSTOM
|
||||
_seed_edit.focus_mode = (
|
||||
Control.FOCUS_ALL
|
||||
if _seed_mode == SeedMode.CUSTOM
|
||||
else Control.FOCUS_NONE
|
||||
)
|
||||
_seed_help.text = (
|
||||
"press random seed again to roll another world."
|
||||
if _seed_mode == SeedMode.RANDOM
|
||||
else "enter text or a number from 1 to %d. the same entry makes the same world."
|
||||
% SaveManagerType.MAX_WORLD_SEED
|
||||
)
|
||||
_refresh_start_state()
|
||||
_configure_controller_focus()
|
||||
|
||||
|
||||
func _refresh_start_state() -> void:
|
||||
_start_button.disabled = (
|
||||
_world_layout == WorldLayoutType.GENERATED
|
||||
and _seed_mode == SeedMode.CUSTOM
|
||||
and parse_seed_text(_seed_edit.text) == 0
|
||||
)
|
||||
|
||||
|
||||
func _configure_controller_focus() -> void:
|
||||
var generated: bool = _world_layout == WorldLayoutType.GENERATED
|
||||
var custom: bool = generated and _seed_mode == SeedMode.CUSTOM
|
||||
_set_neighbors(
|
||||
_generated_button,
|
||||
_generated_button,
|
||||
_starter_button,
|
||||
_generated_button,
|
||||
_random_seed_button if generated else _start_button,
|
||||
)
|
||||
_set_neighbors(
|
||||
_starter_button,
|
||||
_generated_button,
|
||||
_starter_button,
|
||||
_starter_button,
|
||||
_custom_seed_button if generated else _back_button,
|
||||
)
|
||||
_set_neighbors(
|
||||
_random_seed_button,
|
||||
_random_seed_button,
|
||||
_custom_seed_button,
|
||||
_generated_button,
|
||||
_seed_edit if custom else _start_button,
|
||||
)
|
||||
_set_neighbors(
|
||||
_custom_seed_button,
|
||||
_random_seed_button,
|
||||
_custom_seed_button,
|
||||
_starter_button,
|
||||
_seed_edit if custom else _back_button,
|
||||
)
|
||||
_set_neighbors(
|
||||
_seed_edit,
|
||||
_seed_edit,
|
||||
_seed_edit,
|
||||
_custom_seed_button,
|
||||
_start_button,
|
||||
)
|
||||
var action_top: Control = (
|
||||
_seed_edit
|
||||
if custom
|
||||
else _random_seed_button
|
||||
if generated
|
||||
else _generated_button
|
||||
)
|
||||
_set_neighbors(
|
||||
_start_button,
|
||||
_start_button,
|
||||
_back_button,
|
||||
action_top,
|
||||
_start_button,
|
||||
)
|
||||
_set_neighbors(
|
||||
_back_button,
|
||||
_start_button,
|
||||
_back_button,
|
||||
action_top,
|
||||
_back_button,
|
||||
)
|
||||
var traversal: Array[Control] = [
|
||||
_generated_button,
|
||||
_starter_button,
|
||||
]
|
||||
if generated:
|
||||
traversal.append(_random_seed_button)
|
||||
traversal.append(_custom_seed_button)
|
||||
if custom:
|
||||
traversal.append(_seed_edit)
|
||||
traversal.append(_start_button)
|
||||
traversal.append(_back_button)
|
||||
ControllerFocusNavigationType.configure_traversal(traversal)
|
||||
|
||||
|
||||
func _set_neighbors(
|
||||
control: Control,
|
||||
left: Control,
|
||||
right: Control,
|
||||
top: Control,
|
||||
bottom: Control,
|
||||
) -> void:
|
||||
control.focus_neighbor_left = control.get_path_to(left)
|
||||
control.focus_neighbor_right = control.get_path_to(right)
|
||||
control.focus_neighbor_top = control.get_path_to(top)
|
||||
control.focus_neighbor_bottom = control.get_path_to(bottom)
|
||||
1
ui/new_game_setup_page.gd.uid
Normal file
1
ui/new_game_setup_page.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d3iueu4gj6bwo
|
||||
170
ui/new_game_setup_page.tscn
Normal file
170
ui/new_game_setup_page.tscn
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/new_game_setup_page.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||
|
||||
[node name="NewGameSetupPage" type="Control"]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme = ExtResource("2_theme")
|
||||
script = ExtResource("1_script")
|
||||
|
||||
[node name="Paper" type="PanelContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -360.0
|
||||
offset_top = -280.0
|
||||
offset_right = 360.0
|
||||
offset_bottom = 280.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="Paper"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 46
|
||||
theme_override_constants/margin_top = 30
|
||||
theme_override_constants/margin_right = 46
|
||||
theme_override_constants/margin_bottom = 30
|
||||
|
||||
[node name="Layout" type="VBoxContainer" parent="Paper/Margin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Title" type="Label" parent="Paper/Margin/Layout"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_font_sizes/font_size = 32
|
||||
text = "new game"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Prompt" type="Label" parent="Paper/Margin/Layout"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1)
|
||||
theme_override_font_sizes/font_size = 17
|
||||
text = "choose where you want to begin"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="WorldButtons" type="HBoxContainer" parent="Paper/Margin/Layout"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
alignment = 1
|
||||
|
||||
[node name="GeneratedButton" type="Button" parent="Paper/Margin/Layout/WorldButtons"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(230, 58)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
text = "generate a world"
|
||||
|
||||
[node name="StarterButton" type="Button" parent="Paper/Margin/Layout/WorldButtons"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(230, 58)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
text = "starter island"
|
||||
|
||||
[node name="WorldDescription" type="Label" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="SeedSection" type="VBoxContainer" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="SeedLabel" type="Label" parent="Paper/Margin/Layout/SeedSection"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.902, 0.969, 0.969, 1)
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "world seed"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="SeedModes" type="HBoxContainer" parent="Paper/Margin/Layout/SeedSection"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
alignment = 1
|
||||
|
||||
[node name="RandomSeedButton" type="Button" parent="Paper/Margin/Layout/SeedSection/SeedModes"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(190, 48)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
text = "random seed"
|
||||
|
||||
[node name="CustomSeedButton" type="Button" parent="Paper/Margin/Layout/SeedSection/SeedModes"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(190, 48)
|
||||
layout_mode = 2
|
||||
toggle_mode = true
|
||||
text = "enter a seed"
|
||||
|
||||
[node name="SeedEdit" type="LineEdit" parent="Paper/Margin/Layout/SeedSection"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 46)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 20
|
||||
placeholder_text = "number or words"
|
||||
alignment = 1
|
||||
max_length = 64
|
||||
|
||||
[node name="SeedHelp" type="Label" parent="Paper/Margin/Layout/SeedSection"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 28)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="OverwriteWarning" type="Label" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.78, 0.62, 1)
|
||||
theme_override_font_sizes/font_size = 15
|
||||
text = "starting a new game replaces your existing progression."
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Status" type="Label" parent="Paper/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 28)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 0.7, 0.65, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Spacer" type="Control" parent="Paper/Margin/Layout"]
|
||||
custom_minimum_size = Vector2(0, 4)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="Actions" type="HBoxContainer" parent="Paper/Margin/Layout"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
alignment = 1
|
||||
|
||||
[node name="StartButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(190, 54)
|
||||
layout_mode = 2
|
||||
text = "start new game"
|
||||
|
||||
[node name="BackButton" type="Button" parent="Paper/Margin/Layout/Actions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(150, 54)
|
||||
layout_mode = 2
|
||||
text = "back"
|
||||
|
|
@ -70,6 +70,10 @@ func setup(
|
|||
_refresh()
|
||||
|
||||
|
||||
func set_world_interaction(interaction: PlayerStorageInteraction) -> void:
|
||||
_interaction = interaction
|
||||
|
||||
|
||||
func open_storage() -> bool:
|
||||
if (
|
||||
visible
|
||||
|
|
|
|||
|
|
@ -758,11 +758,15 @@ func _progression_import_file_selected(path: String) -> void:
|
|||
dialog.ok_button_text = "import progression"
|
||||
dialog.dialog_text = (
|
||||
"this will replace the current progression after making a backup.\n\n"
|
||||
+ "fish: %d\ndiscovered: %d\nworld seed: %d\n\n"
|
||||
+ "fish: %d\ndiscovered: %d\nworld: %s\nworld seed: %d\n\n"
|
||||
+ "identities, settings, friends, bans, and trusted servers are unchanged."
|
||||
) % [
|
||||
int(inspected.get("catch_count", 0)),
|
||||
int(inspected.get("discovered_species_count", 0)),
|
||||
WorldLayout.display_name(inspected.get(
|
||||
"world_layout",
|
||||
String(WorldLayout.GENERATED),
|
||||
)),
|
||||
int(inspected.get("world_seed", 0)),
|
||||
]
|
||||
dialog.confirmed.connect(_confirm_progression_import.bind(dialog))
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const TitleCreditsPageType = preload("res://ui/title_credits_page.gd")
|
|||
const NetworkSessionType = preload("res://network/network_session.gd")
|
||||
const SavedServerStoreType = preload("res://network/saved_server_store.gd")
|
||||
const JoinGamePageType = preload("res://ui/network/join_game_page.gd")
|
||||
const NewGameSetupPageType = preload("res://ui/new_game_setup_page.gd")
|
||||
const CurrencyPresentationType = preload(
|
||||
"res://ui/currency_presentation.gd"
|
||||
)
|
||||
|
|
@ -94,14 +95,13 @@ const INTRO_BRANDING_TRAVEL_DURATION: float = 2.0
|
|||
const INTRO_BRANDING_START_DELAY: float = 0.35
|
||||
const HOST_CLUSTER_SAFE_MARGIN: float = 24.0
|
||||
|
||||
signal new_game_requested(world_seed: int)
|
||||
signal new_game_requested(world_layout: StringName, world_seed: int)
|
||||
signal continue_game_requested
|
||||
signal quit_requested
|
||||
signal join_game_requested(endpoint: String)
|
||||
|
||||
enum ConfirmationAction {
|
||||
NONE,
|
||||
NEW_GAME,
|
||||
DELETE_SAVE,
|
||||
}
|
||||
|
||||
|
|
@ -142,12 +142,12 @@ enum ConfirmationAction {
|
|||
@onready var _start_prompt_center: CenterContainer = %StartPromptCenter
|
||||
@onready var _start_prompt_label: Label = %StartPromptLabel
|
||||
@onready var _join_game_page: JoinGamePageType = %JoinGamePage
|
||||
@onready var _new_game_setup_page: NewGameSetupPageType = %NewGameSetupPage
|
||||
@onready var _credits_page: TitleCreditsPageType = %CreditsPage
|
||||
|
||||
var _save_manager: SaveManagerType
|
||||
var _settings_manager: SettingsManagerType
|
||||
var _inspection: SaveInspectionType
|
||||
var _pending_new_game_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||
var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE
|
||||
var _action_in_progress: bool = false
|
||||
var _decorative_rng := RandomNumberGenerator.new()
|
||||
|
|
@ -210,6 +210,8 @@ func _ready() -> void:
|
|||
)
|
||||
_new_game_button.pressed.connect(_on_new_game_pressed)
|
||||
_join_game_button.pressed.connect(_open_join_game)
|
||||
_new_game_setup_page.start_requested.connect(_start_configured_new_game)
|
||||
_new_game_setup_page.back_requested.connect(_close_new_game_setup)
|
||||
_settings_button.pressed.connect(_open_settings)
|
||||
_credits_button.pressed.connect(_open_credits)
|
||||
_delete_button.pressed.connect(_on_delete_pressed)
|
||||
|
|
@ -283,6 +285,7 @@ func reopen() -> void:
|
|||
_reset_confirmation()
|
||||
_settings_panel.hide()
|
||||
_join_game_page.close_page()
|
||||
_new_game_setup_page.close_page()
|
||||
_credits_page.close_page()
|
||||
_refresh_save_inspection()
|
||||
show()
|
||||
|
|
@ -311,6 +314,7 @@ func open_join_game_page(endpoint: String = "") -> void:
|
|||
_settings_panel.hide()
|
||||
_confirmation_page.hide_page()
|
||||
_credits_page.close_page()
|
||||
_new_game_setup_page.close_page()
|
||||
_join_game_page.open_page(endpoint)
|
||||
|
||||
|
||||
|
|
@ -326,6 +330,7 @@ func _open_join_game() -> void:
|
|||
if (
|
||||
_action_in_progress
|
||||
or _is_confirmation_active()
|
||||
or _new_game_setup_page.visible
|
||||
or _credits_page.visible
|
||||
):
|
||||
return
|
||||
|
|
@ -346,6 +351,7 @@ func _open_credits() -> void:
|
|||
or _is_confirmation_active()
|
||||
or _settings_panel.visible
|
||||
or _join_game_page.visible
|
||||
or _new_game_setup_page.visible
|
||||
or _credits_page.visible
|
||||
):
|
||||
return
|
||||
|
|
@ -583,6 +589,11 @@ func _input(event: InputEvent) -> void:
|
|||
_join_game_page.request_back()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _new_game_setup_page.visible:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_new_game_setup_page.request_back()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
_title_settings_transition_active
|
||||
or _title_entry_transition_active
|
||||
|
|
@ -629,6 +640,7 @@ func _handle_primary_menu_focus_input(event: InputEvent) -> bool:
|
|||
not _button_center.visible
|
||||
or _is_confirmation_active()
|
||||
or _settings_panel.visible
|
||||
or _new_game_setup_page.visible
|
||||
or _credits_page.visible
|
||||
):
|
||||
return false
|
||||
|
|
@ -1031,19 +1043,59 @@ func _on_new_game_pressed() -> void:
|
|||
"the existing save cannot be accessed safely."
|
||||
)
|
||||
return
|
||||
_pending_new_game_seed = PlayerSaveManager.roll_world_seed()
|
||||
var warning := "start a new world?"
|
||||
if _inspection.status != SaveInspectionType.Status.MISSING:
|
||||
warning += " existing progression will be deleted."
|
||||
warning += "\nworld seed: %d" % _pending_new_game_seed
|
||||
_open_confirmation(
|
||||
ConfirmationAction.NEW_GAME,
|
||||
warning,
|
||||
"start\nnew game",
|
||||
true
|
||||
_open_new_game_setup(
|
||||
_inspection.status != SaveInspectionType.Status.MISSING
|
||||
)
|
||||
|
||||
|
||||
func _open_new_game_setup(has_existing_progression: bool) -> void:
|
||||
_modal_restore_navigation_focus = _navigation_focus_active
|
||||
_set_title_bubbles_interactive(false)
|
||||
_release_primary_menu_focus()
|
||||
_presentation_center.hide()
|
||||
_start_prompt_center.hide()
|
||||
_join_game_page.close_page()
|
||||
_credits_page.close_page()
|
||||
_new_game_setup_page.open_page(has_existing_progression)
|
||||
|
||||
|
||||
func _close_new_game_setup() -> void:
|
||||
_new_game_setup_page.close_page()
|
||||
_presentation_center.show()
|
||||
_button_center.show()
|
||||
_start_prompt_center.hide()
|
||||
_set_title_bubbles_interactive(true)
|
||||
var restore_navigation_focus: bool = _modal_restore_navigation_focus
|
||||
_modal_restore_navigation_focus = false
|
||||
if restore_navigation_focus:
|
||||
_navigation_focus_active = true
|
||||
_new_game_button.grab_focus()
|
||||
else:
|
||||
_navigation_focus_active = false
|
||||
_release_title_focus()
|
||||
_update_continue_stats_visibility()
|
||||
|
||||
|
||||
func _start_configured_new_game(
|
||||
world_layout: StringName,
|
||||
world_seed: int,
|
||||
) -> void:
|
||||
if _action_in_progress or not _new_game_setup_page.visible:
|
||||
return
|
||||
_new_game_setup_page.close_page()
|
||||
_action_in_progress = true
|
||||
new_game_requested.emit(world_layout, world_seed)
|
||||
_action_in_progress = false
|
||||
# Signal delivery is synchronous. A successful start hides this title screen;
|
||||
# if it remains visible, restore the menu so a generation error is recoverable.
|
||||
if visible:
|
||||
_presentation_center.show()
|
||||
_button_center.show()
|
||||
_set_title_bubbles_interactive(true)
|
||||
_navigation_focus_active = true
|
||||
_new_game_button.grab_focus()
|
||||
|
||||
|
||||
func _on_delete_pressed() -> void:
|
||||
if (
|
||||
_action_in_progress
|
||||
|
|
@ -1071,14 +1123,6 @@ func _on_confirmation_accepted() -> void:
|
|||
var action: ConfirmationAction = _confirmation_action
|
||||
_confirmation_page.lock_interaction()
|
||||
_action_in_progress = true
|
||||
if action == ConfirmationAction.NEW_GAME:
|
||||
_confirmation_action = ConfirmationAction.NONE
|
||||
_confirmation_transition_generation += 1
|
||||
_cancel_confirmation_transition()
|
||||
_confirmation_page.hide_page()
|
||||
new_game_requested.emit(_pending_new_game_seed)
|
||||
_action_in_progress = false
|
||||
return
|
||||
if not _save_manager.delete_progression_save():
|
||||
_feedback_label.text = _center_feedback_text(
|
||||
"failed to delete saved progression."
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=23 format=3]
|
||||
[gd_scene load_steps=24 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
[ext_resource type="Texture2D" path="res://ui/icons/main_menu/new_game.png" id="17_new_game"]
|
||||
[ext_resource type="Texture2D" path="res://ui/icons/main_menu/credits.png" id="18_credits"]
|
||||
[ext_resource type="Texture2D" path="res://ui/icons/main_menu/delete_save.png" id="19_delete_save"]
|
||||
[ext_resource type="PackedScene" path="res://ui/new_game_setup_page.tscn" id="20_new_game_setup"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"]
|
||||
shader = ExtResource("4_water_shader")
|
||||
|
|
@ -486,3 +487,13 @@ anchor_right = 1.0
|
|||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="NewGameSetupPage" parent="ResponsiveTitleStage/TitlePresentationScaleRoot" instance=ExtResource("20_new_game_setup")]
|
||||
unique_name_in_owner = true
|
||||
z_index = 220
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
|
|
|||
|
|
@ -7,6 +7,13 @@ const FishingShopInteractionType = preload(
|
|||
const PlayerStorageInteractionType = preload(
|
||||
"res://world/player_storage_interaction.gd"
|
||||
)
|
||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||
const GENERATED_REGION_SCENE: PackedScene = preload(
|
||||
"res://world/generation/generated_world_region.tscn"
|
||||
)
|
||||
const STARTER_ISLAND_REGION_SCENE: PackedScene = preload(
|
||||
"res://world/regions/starter_island_region.tscn"
|
||||
)
|
||||
|
||||
@onready var _regions_root: Node3D = $Regions
|
||||
@onready var _active_region: WorldRegion = _find_active_region()
|
||||
|
|
@ -16,20 +23,25 @@ const PlayerStorageInteractionType = preload(
|
|||
@onready var _world_environment: WorldEnvironment = $Environment/WorldEnvironment
|
||||
@onready var _sun: DirectionalLight3D = $Environment/Sun
|
||||
|
||||
var _world_layout: StringName = WorldLayoutType.GENERATED
|
||||
var _world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||
var _light_performance_profile: bool = false
|
||||
|
||||
|
||||
func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
||||
var triggers: Array[PlayerWaterTrigger] = []
|
||||
for region: WorldRegion in _get_regions():
|
||||
triggers.append_array(region.get_water_recovery_triggers())
|
||||
if _active_region != null:
|
||||
triggers.append_array(_active_region.get_water_recovery_triggers())
|
||||
triggers.append(_below_world_failsafe)
|
||||
return triggers
|
||||
|
||||
|
||||
func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
|
||||
var points: Array[SafeRespawnPoint] = []
|
||||
for region: WorldRegion in _get_regions():
|
||||
points.append_array(region.get_safe_respawn_points())
|
||||
return points
|
||||
return (
|
||||
_active_region.get_safe_respawn_points()
|
||||
if _active_region != null
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
func get_fishing_shop() -> FishingShopInteractionType:
|
||||
|
|
@ -53,14 +65,17 @@ func get_sun() -> DirectionalLight3D:
|
|||
|
||||
|
||||
func set_light_performance_profile(enabled: bool) -> void:
|
||||
_active_region.set_light_performance_profile(enabled)
|
||||
_light_performance_profile = enabled
|
||||
if _active_region != null:
|
||||
_active_region.set_light_performance_profile(enabled)
|
||||
|
||||
|
||||
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
||||
var waters: Array[FishableWaterRegion] = []
|
||||
for region: WorldRegion in _get_regions():
|
||||
waters.append_array(region.get_fishable_water_regions())
|
||||
return waters
|
||||
return (
|
||||
_active_region.get_fishable_water_regions()
|
||||
if _active_region != null
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||
|
|
@ -77,26 +92,44 @@ func get_spawn_surface_triangles(
|
|||
)
|
||||
|
||||
|
||||
func generate_world(seed: int) -> bool:
|
||||
if _active_region == null or not _active_region.has_method("generate_world"):
|
||||
func activate_world(layout: StringName, seed: int) -> bool:
|
||||
if (
|
||||
not WorldLayoutType.is_valid(layout)
|
||||
or seed <= 0
|
||||
or seed > PlayerSaveManager.MAX_WORLD_SEED
|
||||
):
|
||||
return false
|
||||
var generated: bool = bool(_active_region.call("generate_world", seed))
|
||||
if generated:
|
||||
_configure_world_coverage()
|
||||
return generated
|
||||
if _active_region == null or _active_region.region_id != layout:
|
||||
if not _replace_active_region(layout, seed):
|
||||
return false
|
||||
_world_layout = layout
|
||||
_world_seed = seed
|
||||
if layout == WorldLayoutType.GENERATED:
|
||||
if not _active_region.has_method("generate_world"):
|
||||
return false
|
||||
if not bool(_active_region.call("generate_world", seed)):
|
||||
return false
|
||||
_configure_world_coverage()
|
||||
return true
|
||||
|
||||
|
||||
func generate_world(seed: int) -> bool:
|
||||
return activate_world(WorldLayoutType.GENERATED, seed)
|
||||
|
||||
|
||||
func get_world_layout() -> StringName:
|
||||
return _world_layout
|
||||
|
||||
|
||||
func get_generation_seed() -> int:
|
||||
if _active_region != null and _active_region.has_method("get_generation_seed"):
|
||||
return int(_active_region.call("get_generation_seed"))
|
||||
return PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||
return _world_seed
|
||||
|
||||
|
||||
func get_diggable_area_triangles(
|
||||
area_id: StringName,
|
||||
) -> Array[PackedVector3Array]:
|
||||
for region: WorldRegion in _get_regions():
|
||||
var area: DiggableArea3D = region.get_diggable_area(area_id)
|
||||
if _active_region != null:
|
||||
var area: DiggableArea3D = _active_region.get_diggable_area(area_id)
|
||||
if area != null:
|
||||
return area.get_surface_triangles()
|
||||
return []
|
||||
|
|
@ -105,25 +138,18 @@ func get_diggable_area_triangles(
|
|||
func get_gatherable_spawn_positions(
|
||||
anchor_set_id: StringName,
|
||||
) -> PackedVector3Array:
|
||||
for region: WorldRegion in _get_regions():
|
||||
if _active_region != null:
|
||||
var anchor_set: GatherableAnchorSet3D = (
|
||||
region.get_gatherable_anchor_set(anchor_set_id)
|
||||
_active_region.get_gatherable_anchor_set(anchor_set_id)
|
||||
)
|
||||
if anchor_set != null:
|
||||
return anchor_set.get_spawn_positions()
|
||||
return PackedVector3Array()
|
||||
|
||||
|
||||
func _get_regions() -> Array[WorldRegion]:
|
||||
var regions: Array[WorldRegion] = []
|
||||
for child: Node in _regions_root.get_children():
|
||||
var region: WorldRegion = child as WorldRegion
|
||||
if region != null:
|
||||
regions.append(region)
|
||||
return regions
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if _active_region != null:
|
||||
_world_layout = _active_region.region_id
|
||||
_configure_world_coverage()
|
||||
|
||||
|
||||
|
|
@ -135,6 +161,26 @@ func _find_active_region() -> WorldRegion:
|
|||
return null
|
||||
|
||||
|
||||
func _replace_active_region(layout: StringName, seed: int) -> bool:
|
||||
var region_scene: PackedScene = (
|
||||
GENERATED_REGION_SCENE
|
||||
if layout == WorldLayoutType.GENERATED
|
||||
else STARTER_ISLAND_REGION_SCENE
|
||||
)
|
||||
var replacement := region_scene.instantiate() as WorldRegion
|
||||
if replacement == null:
|
||||
return false
|
||||
if layout == WorldLayoutType.GENERATED:
|
||||
replacement.set("initial_seed", seed)
|
||||
if _active_region != null:
|
||||
_regions_root.remove_child(_active_region)
|
||||
_active_region.free()
|
||||
_active_region = replacement
|
||||
_regions_root.add_child(_active_region)
|
||||
_active_region.set_light_performance_profile(_light_performance_profile)
|
||||
return true
|
||||
|
||||
|
||||
func _configure_world_coverage() -> void:
|
||||
if _active_region == null:
|
||||
return
|
||||
|
|
|
|||
29
world/world_layout.gd
Normal file
29
world/world_layout.gd
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
class_name WorldLayout
|
||||
extends RefCounted
|
||||
|
||||
const GENERATED: StringName = &"generated_world"
|
||||
const STARTER_ISLAND: StringName = &"starter_island"
|
||||
|
||||
|
||||
static func is_valid(value: Variant) -> bool:
|
||||
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||
return false
|
||||
var layout := StringName(str(value).strip_edges())
|
||||
return layout in [GENERATED, STARTER_ISLAND]
|
||||
|
||||
|
||||
static func normalized(
|
||||
value: Variant,
|
||||
fallback: StringName = GENERATED,
|
||||
) -> StringName:
|
||||
if not is_valid(value):
|
||||
return fallback
|
||||
return StringName(str(value).strip_edges())
|
||||
|
||||
|
||||
static func display_name(value: Variant) -> String:
|
||||
match normalized(value):
|
||||
STARTER_ISLAND:
|
||||
return "starter island"
|
||||
_:
|
||||
return "generated world"
|
||||
1
world/world_layout.gd.uid
Normal file
1
world/world_layout.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bi2fydywih4lv
|
||||
Loading…
Add table
Add a link
Reference in a new issue