Improve world authoring workflow
This commit is contained in:
parent
67d17074b2
commit
0d26f30172
18 changed files with 682 additions and 138 deletions
|
|
@ -118,7 +118,7 @@ var _new_cast_press_armed: bool = true
|
|||
var _bobber_water_position: Vector3
|
||||
var _fight_start_position: Vector3
|
||||
var _cooldown_status: String = ""
|
||||
var _fishable_query_shape: SphereShape3D = SphereShape3D.new()
|
||||
var _fishable_query_shape: CylinderShape3D = CylinderShape3D.new()
|
||||
var _fish_selector: FishSelectorType = FishSelectorType.new()
|
||||
var _selected_water_region: FishableWaterRegionType
|
||||
var _selection_context: FishingContextType
|
||||
|
|
@ -966,7 +966,7 @@ func _calculate_cast_target(distance: float) -> Vector3:
|
|||
target
|
||||
)
|
||||
if water_region != null:
|
||||
target.y = water_region.surface_height
|
||||
target.y = water_region.get_surface_height()
|
||||
_presentation.set_water_surface_height(target.y)
|
||||
else:
|
||||
_presentation.reset_water_surface_height()
|
||||
|
|
@ -982,11 +982,12 @@ func get_fishable_water_region(
|
|||
target: Vector3,
|
||||
) -> FishableWaterRegionType:
|
||||
_fishable_query_shape.radius = fishable_query_radius
|
||||
_fishable_query_shape.height = preview_ray_length
|
||||
var query := PhysicsShapeQueryParameters3D.new()
|
||||
query.shape = _fishable_query_shape
|
||||
query.transform = Transform3D(
|
||||
Basis.IDENTITY,
|
||||
Vector3(target.x, _presentation.get_water_surface_height(), target.z)
|
||||
Vector3(target.x, target.y, target.z)
|
||||
)
|
||||
query.collision_mask = fishable_surface_mask
|
||||
query.collide_with_areas = true
|
||||
|
|
|
|||
88
world/README-WORLD-AUTHORING.md
Normal file
88
world/README-WORLD-AUTHORING.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# World authoring
|
||||
|
||||
The active test world uses the human-authored starter island through
|
||||
`world/regions/starter_island_region.tscn`.
|
||||
|
||||
## Active composition
|
||||
|
||||
`world/test_world.tscn` owns the gameplay-wide environment, sun, world bounds,
|
||||
and below-world failsafe. `StarterIslandRegion` owns the placed island content:
|
||||
|
||||
```text
|
||||
StarterIslandRegion
|
||||
├── Terrain
|
||||
│ ├── Visual
|
||||
│ └── Collision
|
||||
├── WaterBodies
|
||||
│ ├── Pond
|
||||
│ │ ├── VisualWater
|
||||
│ │ ├── FishingRegion
|
||||
│ │ └── RecoveryRegion
|
||||
│ └── Ocean
|
||||
│ ├── VisualWater
|
||||
│ ├── FishingRegions
|
||||
│ └── RecoveryRegions
|
||||
├── PlayerSpawn
|
||||
├── SafeRespawns
|
||||
└── Interactables
|
||||
├── FishingShopWorld
|
||||
└── PelicanCoolerPerch
|
||||
```
|
||||
|
||||
Move the meaningful feature root rather than one of its implementation
|
||||
children. Child transforms are local offsets owned by that feature.
|
||||
|
||||
## Water bodies
|
||||
|
||||
Select `WaterBodies/Pond` to move or resize the pond. Its transform is the
|
||||
authoritative surface position. The `surface_size`, fishing padding, recovery
|
||||
padding, and recovery depth properties update the visible water and gameplay
|
||||
coverage together. Fishing and recovery derive the surface height from the
|
||||
owned surface transform.
|
||||
|
||||
Select `WaterBodies/Ocean` to move the surrounding water as one feature. Its
|
||||
visual, fishing lobes, and recovery lobes use local transforms beneath the
|
||||
ocean root. The existing multi-lobe footprint remains intentionally explicit;
|
||||
changing that footprint still requires editing its owned lobe children.
|
||||
|
||||
## Placed features
|
||||
|
||||
- `PlayerSpawn` is the authoritative initial player transform. Its rotation
|
||||
controls initial facing.
|
||||
- Each marker under `SafeRespawns` is an authoritative recovery destination.
|
||||
Recovery preserves the player's current facing; marker rotation is
|
||||
intentionally not applied.
|
||||
- `Interactables/FishingShopWorld` owns the shop visual, collision,
|
||||
interaction area, prompt, and entrance marker.
|
||||
- `Interactables/PelicanCoolerPerch` owns the complete Pelican landmark visual.
|
||||
Pelican selling remains a Cooler action and has no world interaction area.
|
||||
- `Terrain` owns both the imported visual and generated terrain collision.
|
||||
- `BelowWorldFailsafe/Coverage` exposes the below-world recovery coverage as a
|
||||
saved collision shape in `world/test_world.tscn`.
|
||||
|
||||
The active `Environment`, `Sun`, and external starter-island grass material
|
||||
remain directly editable through the Inspector. Their tuning is not recreated
|
||||
by runtime scripts.
|
||||
|
||||
## Reusable world props
|
||||
|
||||
Use a single placed `WorldProp` root for trees, rocks, buildings, benches,
|
||||
signs, vegetation, and similar landmarks:
|
||||
|
||||
```text
|
||||
WorldProp
|
||||
├── Visual
|
||||
└── Collision
|
||||
└── CollisionShape3D
|
||||
```
|
||||
|
||||
Add optional interaction areas, labels, and markers beneath the same root.
|
||||
Instance imported GLB visuals beneath `Visual`; keep gameplay collision
|
||||
Godot-owned beneath `Collision`. Place and duplicate the complete prop scene,
|
||||
not its visual child.
|
||||
|
||||
Author reusable assets at scale `(1, 1, 1)`. Uniform root scaling is acceptable
|
||||
for provisional props when visual and collision scale together. Avoid
|
||||
non-uniform root scaling because it can deform collision and imported geometry
|
||||
unpredictably. Make mutable per-instance shapes, meshes, and materials local to
|
||||
the scene so editing one prop does not change unrelated instances.
|
||||
24
world/below_world_failsafe.gd
Normal file
24
world/below_world_failsafe.gd
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
@tool
|
||||
class_name BelowWorldFailsafe
|
||||
extends PlayerWaterTrigger
|
||||
|
||||
@export_group("Failsafe Authoring")
|
||||
## Saved coverage volume that catches players below the playable world.
|
||||
@export_node_path("CollisionShape3D")
|
||||
var coverage_shape_path: NodePath = ^"Coverage"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super()
|
||||
if Engine.is_editor_hint():
|
||||
update_configuration_warnings()
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
var coverage := get_node_or_null(coverage_shape_path) as CollisionShape3D
|
||||
if coverage == null:
|
||||
warnings.append("Add the saved Coverage collision shape.")
|
||||
elif coverage.shape == null:
|
||||
warnings.append("Assign a shape to the failsafe Coverage node.")
|
||||
return warnings
|
||||
1
world/below_world_failsafe.gd.uid
Normal file
1
world/below_world_failsafe.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b71576njgcgyw
|
||||
|
|
@ -1,9 +1,38 @@
|
|||
@tool
|
||||
class_name FishableWaterRegion
|
||||
extends Area3D
|
||||
|
||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||
|
||||
enum SurfaceHeightMode {
|
||||
EXPLICIT,
|
||||
PARENT_GLOBAL_Y,
|
||||
}
|
||||
|
||||
@export var location_tags: Array[StringName] = []
|
||||
@export var fish_pool: FishPoolType
|
||||
@export var selection_priority: int = 0
|
||||
@export_group("Surface")
|
||||
@export var surface_height_mode: SurfaceHeightMode = SurfaceHeightMode.EXPLICIT:
|
||||
set(value):
|
||||
surface_height_mode = value
|
||||
if Engine.is_editor_hint():
|
||||
notify_property_list_changed()
|
||||
## Legacy explicit height used when Surface Height Mode is Explicit.
|
||||
@export var surface_height: float = 0.3
|
||||
|
||||
|
||||
func get_surface_height() -> float:
|
||||
if surface_height_mode == SurfaceHeightMode.PARENT_GLOBAL_Y:
|
||||
var surface_owner := get_parent() as Node3D
|
||||
if surface_owner != null:
|
||||
return surface_owner.global_position.y
|
||||
return surface_height
|
||||
|
||||
|
||||
func _validate_property(property: Dictionary) -> void:
|
||||
if (
|
||||
property.name == "surface_height"
|
||||
and surface_height_mode == SurfaceHeightMode.PARENT_GLOBAL_Y
|
||||
):
|
||||
property.usage = int(property.usage) & ~PROPERTY_USAGE_EDITOR
|
||||
|
|
|
|||
|
|
@ -1,64 +1,78 @@
|
|||
[gd_scene load_steps=10 format=3]
|
||||
[gd_scene load_steps=11 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/fishing_shop_interaction.gd" id="1_shop"]
|
||||
[ext_resource type="Script" path="res://world/world_prop.gd" id="2_prop"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="CounterMesh"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(4.2, 1.25, 1.8)
|
||||
|
||||
[sub_resource type="BoxMesh" id="RoofMesh"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(5.2, 0.35, 3.2)
|
||||
|
||||
[sub_resource type="BoxMesh" id="PostMesh"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(0.28, 3.2, 0.28)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BackMesh"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(5, 2.8, 0.25)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="PrimaryMaterial"]
|
||||
resource_local_to_scene = true
|
||||
albedo_color = Color(0.12, 0.68, 0.62, 1)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="AccentMaterial"]
|
||||
resource_local_to_scene = true
|
||||
albedo_color = Color(1, 0.68, 0.18, 1)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="BackMaterial"]
|
||||
resource_local_to_scene = true
|
||||
albedo_color = Color(0.13, 0.34, 0.38, 1)
|
||||
|
||||
[sub_resource type="SphereShape3D" id="InteractionShape"]
|
||||
resource_local_to_scene = true
|
||||
radius = 3.0
|
||||
|
||||
[sub_resource type="BoxShape3D" id="CounterShape"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(4.2, 1.25, 1.8)
|
||||
|
||||
[node name="FishingShopWorld" type="Node3D"]
|
||||
script = ExtResource("2_prop")
|
||||
collision_root_path = NodePath("Collision")
|
||||
interaction_area_path = NodePath("InteractionArea")
|
||||
entrance_marker_path = NodePath("EntranceMarker")
|
||||
|
||||
[node name="Visuals" type="Node3D" parent="."]
|
||||
[node name="Visual" type="Node3D" parent="."]
|
||||
|
||||
[node name="Counter" type="MeshInstance3D" parent="Visuals"]
|
||||
[node name="Counter" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(0, 0.625, 0)
|
||||
mesh = SubResource("CounterMesh")
|
||||
material_override = SubResource("PrimaryMaterial")
|
||||
|
||||
[node name="Back" type="MeshInstance3D" parent="Visuals"]
|
||||
[node name="Back" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(0, 1.4, 1.25)
|
||||
mesh = SubResource("BackMesh")
|
||||
material_override = SubResource("BackMaterial")
|
||||
|
||||
[node name="Roof" type="MeshInstance3D" parent="Visuals"]
|
||||
[node name="Roof" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(0, 3.15, 0.35)
|
||||
mesh = SubResource("RoofMesh")
|
||||
material_override = SubResource("AccentMaterial")
|
||||
|
||||
[node name="WestPost" type="MeshInstance3D" parent="Visuals"]
|
||||
[node name="WestPost" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(-2.15, 1.6, -0.8)
|
||||
mesh = SubResource("PostMesh")
|
||||
material_override = SubResource("PrimaryMaterial")
|
||||
|
||||
[node name="EastPost" type="MeshInstance3D" parent="Visuals"]
|
||||
[node name="EastPost" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(2.15, 1.6, -0.8)
|
||||
mesh = SubResource("PostMesh")
|
||||
material_override = SubResource("PrimaryMaterial")
|
||||
|
||||
[node name="ShopName" type="Label3D" parent="Visuals"]
|
||||
[node name="ShopName" type="Label3D" parent="Visual"]
|
||||
position = Vector3(0, 3.7, 0)
|
||||
text = "fishing shop"
|
||||
font_size = 42
|
||||
|
|
@ -66,18 +80,21 @@ outline_size = 9
|
|||
billboard = 1
|
||||
no_depth_test = true
|
||||
|
||||
[node name="StaticCollision" type="StaticBody3D" parent="."]
|
||||
[node name="Collision" type="StaticBody3D" parent="."]
|
||||
|
||||
[node name="CounterShape" type="CollisionShape3D" parent="StaticCollision"]
|
||||
[node name="CounterShape" type="CollisionShape3D" parent="Collision"]
|
||||
position = Vector3(0, 0.625, 0)
|
||||
shape = SubResource("CounterShape")
|
||||
|
||||
[node name="FishingShopInteraction" type="Area3D" parent="."]
|
||||
[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="CollisionShape3D" type="CollisionShape3D" parent="FishingShopInteraction"]
|
||||
[node name="InteractionShape" type="CollisionShape3D" parent="InteractionArea"]
|
||||
shape = SubResource("InteractionShape")
|
||||
|
||||
[node name="EntranceMarker" type="Marker3D" parent="."]
|
||||
position = Vector3(0, 0, -2)
|
||||
|
|
|
|||
|
|
@ -1,39 +1,46 @@
|
|||
[gd_scene load_steps=5 format=3]
|
||||
[gd_scene load_steps=6 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/world_prop.gd" id="1_prop"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="PerchMesh"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(5, 0.7, 2.2)
|
||||
|
||||
[sub_resource type="CylinderMesh" id="PostMesh"]
|
||||
resource_local_to_scene = true
|
||||
top_radius = 0.18
|
||||
bottom_radius = 0.24
|
||||
height = 2.5
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="WoodMaterial"]
|
||||
resource_local_to_scene = true
|
||||
albedo_color = Color(0.46, 0.28, 0.13, 1)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="SignMaterial"]
|
||||
resource_local_to_scene = true
|
||||
albedo_color = Color(0.95, 0.82, 0.42, 1)
|
||||
|
||||
[node name="PelicanCoolerPerch" type="Node3D"]
|
||||
script = ExtResource("1_prop")
|
||||
|
||||
[node name="Visuals" type="Node3D" parent="."]
|
||||
[node name="Visual" type="Node3D" parent="."]
|
||||
|
||||
[node name="Perch" type="MeshInstance3D" parent="Visuals"]
|
||||
[node name="Perch" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(0, 1.65, 0)
|
||||
mesh = SubResource("PerchMesh")
|
||||
material_override = SubResource("WoodMaterial")
|
||||
|
||||
[node name="WestPost" type="MeshInstance3D" parent="Visuals"]
|
||||
[node name="WestPost" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(-1.8, 0.7, 0)
|
||||
mesh = SubResource("PostMesh")
|
||||
material_override = SubResource("WoodMaterial")
|
||||
|
||||
[node name="EastPost" type="MeshInstance3D" parent="Visuals"]
|
||||
[node name="EastPost" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(1.8, 0.7, 0)
|
||||
mesh = SubResource("PostMesh")
|
||||
material_override = SubResource("WoodMaterial")
|
||||
|
||||
[node name="Sign" type="Label3D" parent="Visuals"]
|
||||
[node name="Sign" type="Label3D" parent="Visual"]
|
||||
position = Vector3(0, 2.55, 0)
|
||||
text = "sell to pelicans\nfrom the cooler"
|
||||
font_size = 30
|
||||
|
|
@ -41,7 +48,7 @@ outline_size = 8
|
|||
billboard = 1
|
||||
no_depth_test = true
|
||||
|
||||
[node name="ConvenienceLabel" type="Label3D" parent="Visuals"]
|
||||
[node name="ConvenienceLabel" type="Label3D" parent="Visual"]
|
||||
position = Vector3(0, 1.65, -1.15)
|
||||
text = "available anywhere • 0.25x"
|
||||
font_size = 20
|
||||
|
|
|
|||
|
|
@ -1,9 +1,24 @@
|
|||
@tool
|
||||
class_name PlayerWaterTrigger
|
||||
extends Area3D
|
||||
|
||||
signal recovery_requested(player: Player, surface_height: float)
|
||||
|
||||
enum SurfaceHeightMode {
|
||||
EXPLICIT,
|
||||
PARENT_GLOBAL_Y,
|
||||
}
|
||||
|
||||
@export_group("Surface")
|
||||
@export var surface_height_mode: SurfaceHeightMode = SurfaceHeightMode.EXPLICIT:
|
||||
set(value):
|
||||
surface_height_mode = value
|
||||
if Engine.is_editor_hint():
|
||||
notify_property_list_changed()
|
||||
## Legacy explicit height used when Surface Height Mode is Explicit.
|
||||
@export var surface_height: float = 0.2
|
||||
@export_group("Recovery Trigger")
|
||||
## Recovery begins when the player's body center reaches this depth.
|
||||
@export_range(0.0, 2.0, 0.05) var entry_depth_threshold: float = 0.35
|
||||
|
||||
var _tracked_players: Array[Player] = []
|
||||
|
|
@ -11,11 +26,15 @@ var _triggered_players: Dictionary[StringName, bool] = {}
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
set_physics_process(false)
|
||||
return
|
||||
body_entered.connect(_on_body_entered)
|
||||
body_exited.connect(_on_body_exited)
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
var active_surface_height: float = get_surface_height()
|
||||
for player: Player in _tracked_players.duplicate():
|
||||
if not is_instance_valid(player):
|
||||
_tracked_players.erase(player)
|
||||
|
|
@ -25,10 +44,26 @@ func _physics_process(_delta: float) -> void:
|
|||
continue
|
||||
if (
|
||||
player.get_body_center_position().y
|
||||
<= surface_height - entry_depth_threshold
|
||||
<= active_surface_height - entry_depth_threshold
|
||||
):
|
||||
_triggered_players[player_key] = true
|
||||
recovery_requested.emit(player, surface_height)
|
||||
recovery_requested.emit(player, active_surface_height)
|
||||
|
||||
|
||||
func get_surface_height() -> float:
|
||||
if surface_height_mode == SurfaceHeightMode.PARENT_GLOBAL_Y:
|
||||
var surface_owner := get_parent() as Node3D
|
||||
if surface_owner != null:
|
||||
return surface_owner.global_position.y
|
||||
return surface_height
|
||||
|
||||
|
||||
func _validate_property(property: Dictionary) -> void:
|
||||
if (
|
||||
property.name == "surface_height"
|
||||
and surface_height_mode == SurfaceHeightMode.PARENT_GLOBAL_Y
|
||||
):
|
||||
property.usage = int(property.usage) & ~PROPERTY_USAGE_EDITOR
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
@tool
|
||||
class_name StarterIslandRegion
|
||||
extends GrayboxRegion
|
||||
extends WorldRegion
|
||||
|
||||
const FishingShopInteractionType = preload(
|
||||
"res://world/fishing_shop_interaction.gd"
|
||||
)
|
||||
|
||||
@export var visual_mesh_path: NodePath = (
|
||||
^"Visuals/StarterIslandModel/starter_island"
|
||||
@export_group("Owned Nodes")
|
||||
@export_node_path("MeshInstance3D")
|
||||
var visual_mesh_path: NodePath = (
|
||||
^"Terrain/Visual/starter_island"
|
||||
)
|
||||
@export var terrain_collision_shape_path: NodePath = (
|
||||
^"StaticCollision/Terrain/Shape"
|
||||
@export_node_path("CollisionShape3D")
|
||||
var terrain_collision_shape_path: NodePath = (
|
||||
^"Terrain/Collision/Shape"
|
||||
)
|
||||
@export var player_spawn_path: NodePath = ^"PlayerSpawn"
|
||||
@export var fishing_shop_path: NodePath = (
|
||||
^"Interactables/FishingShopWorld/FishingShopInteraction"
|
||||
@export_node_path("Marker3D")
|
||||
var player_spawn_path: NodePath = ^"PlayerSpawn"
|
||||
@export_node_path("Area3D")
|
||||
var fishing_shop_path: NodePath = (
|
||||
^"Interactables/FishingShopWorld/InteractionArea"
|
||||
)
|
||||
@export var pelican_landmark_path: NodePath = (
|
||||
@export_node_path("Node3D")
|
||||
var pelican_landmark_path: NodePath = (
|
||||
^"Interactables/PelicanCoolerPerch"
|
||||
)
|
||||
|
||||
|
|
@ -27,6 +34,8 @@ const FishingShopInteractionType = preload(
|
|||
func _ready() -> void:
|
||||
_apply_grass_material()
|
||||
_build_terrain_collision()
|
||||
if Engine.is_editor_hint():
|
||||
update_configuration_warnings()
|
||||
|
||||
|
||||
func get_player_spawn_transform() -> Transform3D:
|
||||
|
|
@ -91,3 +100,25 @@ func _build_terrain_collision() -> void:
|
|||
push_error("Starter island terrain collision could not be created.")
|
||||
return
|
||||
collision_shape.shape = terrain_shape
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
var visual_mesh := get_node_or_null(visual_mesh_path) as MeshInstance3D
|
||||
if visual_mesh == null or visual_mesh.mesh == null:
|
||||
warnings.append("Terrain/Visual must provide the island mesh.")
|
||||
elif grass_surface_index >= visual_mesh.mesh.get_surface_count():
|
||||
warnings.append("Grass surface index is outside the terrain mesh.")
|
||||
elif visual_mesh.mesh.surface_get_name(grass_surface_index) != "grass":
|
||||
warnings.append("Grass surface index must identify the grass surface.")
|
||||
if grass_material == null:
|
||||
warnings.append("Assign the starter-island grass material.")
|
||||
if get_node_or_null(terrain_collision_shape_path) == null:
|
||||
warnings.append("Terrain/Collision must provide a collision shape.")
|
||||
if get_node_or_null(player_spawn_path) == null:
|
||||
warnings.append("PlayerSpawn marker is missing.")
|
||||
if get_node_or_null(fishing_shop_path) == null:
|
||||
warnings.append("Fishing Shop placement is missing.")
|
||||
if get_node_or_null(pelican_landmark_path) == null:
|
||||
warnings.append("Pelican placement is missing.")
|
||||
return warnings
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
60
world/regions/world_region.gd
Normal file
60
world/regions/world_region.gd
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
@tool
|
||||
class_name WorldRegion
|
||||
extends Node3D
|
||||
|
||||
@export var region_id: StringName
|
||||
@export var fishable_water_root: NodePath = ^"FishingWater"
|
||||
@export var water_recovery_root: NodePath = ^"WaterRecovery"
|
||||
@export var safe_respawns_root: NodePath = ^"SafeRespawns"
|
||||
|
||||
|
||||
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
||||
var regions: Array[FishableWaterRegion] = []
|
||||
var root: Node = get_node_or_null(fishable_water_root)
|
||||
if root == null:
|
||||
return regions
|
||||
_collect_fishable_water_regions(root, regions)
|
||||
return regions
|
||||
|
||||
|
||||
func get_water_recovery_triggers() -> Array[PlayerWaterTrigger]:
|
||||
var triggers: Array[PlayerWaterTrigger] = []
|
||||
var root: Node = get_node_or_null(water_recovery_root)
|
||||
if root == null:
|
||||
return triggers
|
||||
_collect_water_recovery_triggers(root, triggers)
|
||||
return triggers
|
||||
|
||||
|
||||
func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
|
||||
var points: Array[SafeRespawnPoint] = []
|
||||
var root: Node = get_node_or_null(safe_respawns_root)
|
||||
if root == null:
|
||||
return points
|
||||
for child: Node in root.get_children():
|
||||
var point: SafeRespawnPoint = child as SafeRespawnPoint
|
||||
if point != null:
|
||||
points.append(point)
|
||||
return points
|
||||
|
||||
|
||||
func _collect_fishable_water_regions(
|
||||
root: Node,
|
||||
regions: Array[FishableWaterRegion],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var water := child as FishableWaterRegion
|
||||
if water != null:
|
||||
regions.append(water)
|
||||
_collect_fishable_water_regions(child, regions)
|
||||
|
||||
|
||||
func _collect_water_recovery_triggers(
|
||||
root: Node,
|
||||
triggers: Array[PlayerWaterTrigger],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var trigger := child as PlayerWaterTrigger
|
||||
if trigger != null:
|
||||
triggers.append(trigger)
|
||||
_collect_water_recovery_triggers(child, triggers)
|
||||
1
world/regions/world_region.gd.uid
Normal file
1
world/regions/world_region.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bb1j0uhvrbf5n
|
||||
|
|
@ -19,7 +19,7 @@ const FishingShopInteractionType = preload(
|
|||
|
||||
func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
||||
var triggers: Array[PlayerWaterTrigger] = []
|
||||
for region: GrayboxRegion in _get_regions():
|
||||
for region: WorldRegion in _get_regions():
|
||||
triggers.append_array(region.get_water_recovery_triggers())
|
||||
triggers.append(_below_world_failsafe)
|
||||
return triggers
|
||||
|
|
@ -27,7 +27,7 @@ func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
|||
|
||||
func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
|
||||
var points: Array[SafeRespawnPoint] = []
|
||||
for region: GrayboxRegion in _get_regions():
|
||||
for region: WorldRegion in _get_regions():
|
||||
points.append_array(region.get_safe_respawn_points())
|
||||
return points
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ func get_player_spawn_transform() -> Transform3D:
|
|||
|
||||
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
||||
var waters: Array[FishableWaterRegion] = []
|
||||
for region: GrayboxRegion in _get_regions():
|
||||
for region: WorldRegion in _get_regions():
|
||||
waters.append_array(region.get_fishable_water_regions())
|
||||
return waters
|
||||
|
||||
|
|
@ -51,10 +51,10 @@ func get_pelican_convenience_landmark() -> Node3D:
|
|||
return _starter_island.get_pelican_landmark()
|
||||
|
||||
|
||||
func _get_regions() -> Array[GrayboxRegion]:
|
||||
var regions: Array[GrayboxRegion] = []
|
||||
func _get_regions() -> Array[WorldRegion]:
|
||||
var regions: Array[WorldRegion] = []
|
||||
for child: Node in _regions_root.get_children():
|
||||
var region: GrayboxRegion = child as GrayboxRegion
|
||||
var region: WorldRegion = child as WorldRegion
|
||||
if region != null:
|
||||
regions.append(region)
|
||||
return regions
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="Script" path="res://world/test_world.gd" id="1_world"]
|
||||
[ext_resource type="PackedScene" path="res://world/regions/starter_island_region.tscn" id="2_island"]
|
||||
[ext_resource type="Script" path="res://world/player_water_trigger.gd" id="3_water_trigger"]
|
||||
[ext_resource type="Script" path="res://world/below_world_failsafe.gd" id="3_water_trigger"]
|
||||
|
||||
[sub_resource type="Environment" id="Environment"]
|
||||
background_mode = 1
|
||||
|
|
@ -60,7 +60,7 @@ collision_mask = 2
|
|||
script = ExtResource("3_water_trigger")
|
||||
surface_height = 0.3
|
||||
|
||||
[node name="FailsafeShape" type="CollisionShape3D" parent="Safety/BelowWorldFailsafe"]
|
||||
[node name="Coverage" type="CollisionShape3D" parent="Safety/BelowWorldFailsafe"]
|
||||
shape = SubResource("FailsafeShape")
|
||||
|
||||
[node name="WorldBounds" type="Node3D" parent="."]
|
||||
|
|
|
|||
158
world/water_body_authoring.gd
Normal file
158
world/water_body_authoring.gd
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
@tool
|
||||
class_name WaterBodyAuthoring
|
||||
extends Node3D
|
||||
|
||||
@export_group("Surface")
|
||||
## Visible water footprint in meters.
|
||||
@export_custom(
|
||||
PROPERTY_HINT_RANGE,
|
||||
"0.1,200.0,0.1,or_greater,suffix:m",
|
||||
)
|
||||
var surface_size: Vector2 = Vector2(10.0, 10.0):
|
||||
set(value):
|
||||
surface_size = Vector2(maxf(value.x, 0.1), maxf(value.y, 0.1))
|
||||
_sync_owned_nodes()
|
||||
## Material applied to the visible water surface.
|
||||
@export var water_material: Material:
|
||||
set(value):
|
||||
water_material = value
|
||||
_sync_owned_nodes()
|
||||
|
||||
@export_group("Fishing Coverage")
|
||||
## Extra fishable coverage beyond each edge of the visible surface.
|
||||
@export_custom(
|
||||
PROPERTY_HINT_RANGE,
|
||||
"0.0,20.0,0.1,or_greater,suffix:m",
|
||||
)
|
||||
var fishing_padding: Vector2 = Vector2.ZERO:
|
||||
set(value):
|
||||
fishing_padding = Vector2(maxf(value.x, 0.0), maxf(value.y, 0.0))
|
||||
_sync_owned_nodes()
|
||||
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
|
||||
var fishing_depth: float = 4.0:
|
||||
set(value):
|
||||
fishing_depth = maxf(value, 0.1)
|
||||
_sync_owned_nodes()
|
||||
|
||||
@export_group("Recovery Coverage")
|
||||
## Extra recovery coverage beyond each edge of the visible surface.
|
||||
@export_custom(
|
||||
PROPERTY_HINT_RANGE,
|
||||
"0.0,20.0,0.1,or_greater,suffix:m",
|
||||
)
|
||||
var recovery_padding: Vector2 = Vector2.ZERO:
|
||||
set(value):
|
||||
recovery_padding = Vector2(maxf(value.x, 0.0), maxf(value.y, 0.0))
|
||||
_sync_owned_nodes()
|
||||
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
|
||||
var recovery_depth: float = 4.8:
|
||||
set(value):
|
||||
recovery_depth = maxf(value, 0.1)
|
||||
_sync_owned_nodes()
|
||||
|
||||
@export_group("Owned Nodes")
|
||||
@export_node_path("MeshInstance3D")
|
||||
var visual_water_path: NodePath = ^"VisualWater"
|
||||
@export_node_path("CollisionShape3D")
|
||||
var fishing_shape_path: NodePath = ^"FishingRegion/Shape"
|
||||
@export_node_path("Area3D")
|
||||
var recovery_region_path: NodePath = ^"RecoveryRegion"
|
||||
@export_node_path("CollisionShape3D")
|
||||
var recovery_shape_path: NodePath = ^"RecoveryRegion/Shape"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_sync_owned_nodes()
|
||||
if Engine.is_editor_hint():
|
||||
update_configuration_warnings()
|
||||
|
||||
|
||||
func _sync_owned_nodes() -> void:
|
||||
if not is_inside_tree():
|
||||
return
|
||||
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
|
||||
if visual_water != null:
|
||||
var plane_mesh := visual_water.mesh as PlaneMesh
|
||||
if plane_mesh != null:
|
||||
plane_mesh.size = surface_size
|
||||
visual_water.material_override = water_material
|
||||
|
||||
var fishing_shape_node := (
|
||||
get_node_or_null(fishing_shape_path) as CollisionShape3D
|
||||
)
|
||||
if fishing_shape_node != null:
|
||||
var fishing_shape := fishing_shape_node.shape as BoxShape3D
|
||||
if fishing_shape != null:
|
||||
fishing_shape.size = Vector3(
|
||||
surface_size.x + fishing_padding.x * 2.0,
|
||||
fishing_depth,
|
||||
surface_size.y + fishing_padding.y * 2.0
|
||||
)
|
||||
|
||||
var recovery_region := (
|
||||
get_node_or_null(recovery_region_path) as Area3D
|
||||
)
|
||||
if recovery_region != null:
|
||||
recovery_region.position.y = -recovery_depth * 0.5
|
||||
var recovery_shape_node := (
|
||||
get_node_or_null(recovery_shape_path) as CollisionShape3D
|
||||
)
|
||||
if recovery_shape_node != null:
|
||||
var recovery_shape := recovery_shape_node.shape as BoxShape3D
|
||||
if recovery_shape != null:
|
||||
recovery_shape.size = Vector3(
|
||||
surface_size.x + recovery_padding.x * 2.0,
|
||||
recovery_depth,
|
||||
surface_size.y + recovery_padding.y * 2.0
|
||||
)
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
update_configuration_warnings()
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
if not scale.is_equal_approx(Vector3.ONE):
|
||||
warnings.append(
|
||||
"Keep root scale at 1; resize water with Surface Size."
|
||||
)
|
||||
var visual_water := get_node_or_null(visual_water_path) as MeshInstance3D
|
||||
if visual_water == null or not visual_water.mesh is PlaneMesh:
|
||||
warnings.append("VisualWater must provide a PlaneMesh.")
|
||||
var fishing_shape := get_node_or_null(fishing_shape_path) as CollisionShape3D
|
||||
if fishing_shape == null or not fishing_shape.shape is BoxShape3D:
|
||||
warnings.append("FishingRegion must provide a BoxShape3D.")
|
||||
var recovery_region := (
|
||||
get_node_or_null(recovery_region_path) as PlayerWaterTrigger
|
||||
)
|
||||
if recovery_region == null:
|
||||
warnings.append("RecoveryRegion must use PlayerWaterTrigger.")
|
||||
elif (
|
||||
recovery_region.surface_height_mode
|
||||
!= PlayerWaterTrigger.SurfaceHeightMode.PARENT_GLOBAL_Y
|
||||
):
|
||||
warnings.append(
|
||||
"RecoveryRegion must derive surface height from its parent."
|
||||
)
|
||||
var recovery_shape := (
|
||||
get_node_or_null(recovery_shape_path) as CollisionShape3D
|
||||
)
|
||||
if recovery_shape == null or not recovery_shape.shape is BoxShape3D:
|
||||
warnings.append("RecoveryRegion must provide a BoxShape3D.")
|
||||
var fishing_region := (
|
||||
fishing_shape.get_parent() as FishableWaterRegion
|
||||
if fishing_shape != null
|
||||
else null
|
||||
)
|
||||
if fishing_region == null:
|
||||
warnings.append("FishingRegion must use FishableWaterRegion.")
|
||||
elif (
|
||||
fishing_region.surface_height_mode
|
||||
!= FishableWaterRegion.SurfaceHeightMode.PARENT_GLOBAL_Y
|
||||
):
|
||||
warnings.append(
|
||||
"FishingRegion must derive surface height from its parent."
|
||||
)
|
||||
if water_material == null:
|
||||
warnings.append("Assign a water material.")
|
||||
return warnings
|
||||
1
world/water_body_authoring.gd.uid
Normal file
1
world/water_body_authoring.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dtcgnxl1kmdu1
|
||||
62
world/world_prop.gd
Normal file
62
world/world_prop.gd
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
@tool
|
||||
class_name WorldProp
|
||||
extends Node3D
|
||||
|
||||
@export_group("Owned Nodes")
|
||||
@export_node_path("Node3D")
|
||||
var visual_root_path: NodePath = ^"Visual"
|
||||
@export_node_path("Node3D")
|
||||
var collision_root_path: NodePath
|
||||
@export_node_path("Area3D")
|
||||
var interaction_area_path: NodePath
|
||||
@export_node_path("Marker3D")
|
||||
var entrance_marker_path: NodePath
|
||||
|
||||
@export_group("Scaling")
|
||||
## Enable only when this prop explicitly supports non-uniform root scaling.
|
||||
@export var allow_non_uniform_scale: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
return
|
||||
set_notify_transform(true)
|
||||
update_configuration_warnings()
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if Engine.is_editor_hint() and what == NOTIFICATION_TRANSFORM_CHANGED:
|
||||
update_configuration_warnings()
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
if get_node_or_null(visual_root_path) == null:
|
||||
warnings.append("Assign a valid Visual root.")
|
||||
if (
|
||||
not collision_root_path.is_empty()
|
||||
and get_node_or_null(collision_root_path) == null
|
||||
):
|
||||
warnings.append("Assign a valid Collision root.")
|
||||
if (
|
||||
not interaction_area_path.is_empty()
|
||||
and get_node_or_null(interaction_area_path) == null
|
||||
):
|
||||
warnings.append("Assign a valid InteractionArea.")
|
||||
if (
|
||||
not entrance_marker_path.is_empty()
|
||||
and get_node_or_null(entrance_marker_path) == null
|
||||
):
|
||||
warnings.append("Assign a valid EntranceMarker.")
|
||||
if not allow_non_uniform_scale and not _has_uniform_scale():
|
||||
warnings.append(
|
||||
"Use uniform root scale so visuals and collision stay aligned."
|
||||
)
|
||||
return warnings
|
||||
|
||||
|
||||
func _has_uniform_scale() -> bool:
|
||||
return (
|
||||
is_equal_approx(scale.x, scale.y)
|
||||
and is_equal_approx(scale.y, scale.z)
|
||||
)
|
||||
1
world/world_prop.gd.uid
Normal file
1
world/world_prop.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c1p4583452rpa
|
||||
Loading…
Add table
Add a link
Reference in a new issue