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 _bobber_water_position: Vector3
|
||||||
var _fight_start_position: Vector3
|
var _fight_start_position: Vector3
|
||||||
var _cooldown_status: String = ""
|
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 _fish_selector: FishSelectorType = FishSelectorType.new()
|
||||||
var _selected_water_region: FishableWaterRegionType
|
var _selected_water_region: FishableWaterRegionType
|
||||||
var _selection_context: FishingContextType
|
var _selection_context: FishingContextType
|
||||||
|
|
@ -966,7 +966,7 @@ func _calculate_cast_target(distance: float) -> Vector3:
|
||||||
target
|
target
|
||||||
)
|
)
|
||||||
if water_region != null:
|
if water_region != null:
|
||||||
target.y = water_region.surface_height
|
target.y = water_region.get_surface_height()
|
||||||
_presentation.set_water_surface_height(target.y)
|
_presentation.set_water_surface_height(target.y)
|
||||||
else:
|
else:
|
||||||
_presentation.reset_water_surface_height()
|
_presentation.reset_water_surface_height()
|
||||||
|
|
@ -982,11 +982,12 @@ func get_fishable_water_region(
|
||||||
target: Vector3,
|
target: Vector3,
|
||||||
) -> FishableWaterRegionType:
|
) -> FishableWaterRegionType:
|
||||||
_fishable_query_shape.radius = fishable_query_radius
|
_fishable_query_shape.radius = fishable_query_radius
|
||||||
|
_fishable_query_shape.height = preview_ray_length
|
||||||
var query := PhysicsShapeQueryParameters3D.new()
|
var query := PhysicsShapeQueryParameters3D.new()
|
||||||
query.shape = _fishable_query_shape
|
query.shape = _fishable_query_shape
|
||||||
query.transform = Transform3D(
|
query.transform = Transform3D(
|
||||||
Basis.IDENTITY,
|
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.collision_mask = fishable_surface_mask
|
||||||
query.collide_with_areas = true
|
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
|
class_name FishableWaterRegion
|
||||||
extends Area3D
|
extends Area3D
|
||||||
|
|
||||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
|
|
||||||
|
enum SurfaceHeightMode {
|
||||||
|
EXPLICIT,
|
||||||
|
PARENT_GLOBAL_Y,
|
||||||
|
}
|
||||||
|
|
||||||
@export var location_tags: Array[StringName] = []
|
@export var location_tags: Array[StringName] = []
|
||||||
@export var fish_pool: FishPoolType
|
@export var fish_pool: FishPoolType
|
||||||
@export var selection_priority: int = 0
|
@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
|
@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/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"]
|
[sub_resource type="BoxMesh" id="CounterMesh"]
|
||||||
|
resource_local_to_scene = true
|
||||||
size = Vector3(4.2, 1.25, 1.8)
|
size = Vector3(4.2, 1.25, 1.8)
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="RoofMesh"]
|
[sub_resource type="BoxMesh" id="RoofMesh"]
|
||||||
|
resource_local_to_scene = true
|
||||||
size = Vector3(5.2, 0.35, 3.2)
|
size = Vector3(5.2, 0.35, 3.2)
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="PostMesh"]
|
[sub_resource type="BoxMesh" id="PostMesh"]
|
||||||
|
resource_local_to_scene = true
|
||||||
size = Vector3(0.28, 3.2, 0.28)
|
size = Vector3(0.28, 3.2, 0.28)
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BackMesh"]
|
[sub_resource type="BoxMesh" id="BackMesh"]
|
||||||
|
resource_local_to_scene = true
|
||||||
size = Vector3(5, 2.8, 0.25)
|
size = Vector3(5, 2.8, 0.25)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="PrimaryMaterial"]
|
[sub_resource type="StandardMaterial3D" id="PrimaryMaterial"]
|
||||||
|
resource_local_to_scene = true
|
||||||
albedo_color = Color(0.12, 0.68, 0.62, 1)
|
albedo_color = Color(0.12, 0.68, 0.62, 1)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="AccentMaterial"]
|
[sub_resource type="StandardMaterial3D" id="AccentMaterial"]
|
||||||
|
resource_local_to_scene = true
|
||||||
albedo_color = Color(1, 0.68, 0.18, 1)
|
albedo_color = Color(1, 0.68, 0.18, 1)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="BackMaterial"]
|
[sub_resource type="StandardMaterial3D" id="BackMaterial"]
|
||||||
|
resource_local_to_scene = true
|
||||||
albedo_color = Color(0.13, 0.34, 0.38, 1)
|
albedo_color = Color(0.13, 0.34, 0.38, 1)
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="InteractionShape"]
|
[sub_resource type="SphereShape3D" id="InteractionShape"]
|
||||||
|
resource_local_to_scene = true
|
||||||
radius = 3.0
|
radius = 3.0
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="CounterShape"]
|
[sub_resource type="BoxShape3D" id="CounterShape"]
|
||||||
|
resource_local_to_scene = true
|
||||||
size = Vector3(4.2, 1.25, 1.8)
|
size = Vector3(4.2, 1.25, 1.8)
|
||||||
|
|
||||||
[node name="FishingShopWorld" type="Node3D"]
|
[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)
|
position = Vector3(0, 0.625, 0)
|
||||||
mesh = SubResource("CounterMesh")
|
mesh = SubResource("CounterMesh")
|
||||||
material_override = SubResource("PrimaryMaterial")
|
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)
|
position = Vector3(0, 1.4, 1.25)
|
||||||
mesh = SubResource("BackMesh")
|
mesh = SubResource("BackMesh")
|
||||||
material_override = SubResource("BackMaterial")
|
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)
|
position = Vector3(0, 3.15, 0.35)
|
||||||
mesh = SubResource("RoofMesh")
|
mesh = SubResource("RoofMesh")
|
||||||
material_override = SubResource("AccentMaterial")
|
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)
|
position = Vector3(-2.15, 1.6, -0.8)
|
||||||
mesh = SubResource("PostMesh")
|
mesh = SubResource("PostMesh")
|
||||||
material_override = SubResource("PrimaryMaterial")
|
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)
|
position = Vector3(2.15, 1.6, -0.8)
|
||||||
mesh = SubResource("PostMesh")
|
mesh = SubResource("PostMesh")
|
||||||
material_override = SubResource("PrimaryMaterial")
|
material_override = SubResource("PrimaryMaterial")
|
||||||
|
|
||||||
[node name="ShopName" type="Label3D" parent="Visuals"]
|
[node name="ShopName" type="Label3D" parent="Visual"]
|
||||||
position = Vector3(0, 3.7, 0)
|
position = Vector3(0, 3.7, 0)
|
||||||
text = "fishing shop"
|
text = "fishing shop"
|
||||||
font_size = 42
|
font_size = 42
|
||||||
|
|
@ -66,18 +80,21 @@ outline_size = 9
|
||||||
billboard = 1
|
billboard = 1
|
||||||
no_depth_test = true
|
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)
|
position = Vector3(0, 0.625, 0)
|
||||||
shape = SubResource("CounterShape")
|
shape = SubResource("CounterShape")
|
||||||
|
|
||||||
[node name="FishingShopInteraction" type="Area3D" parent="."]
|
[node name="InteractionArea" type="Area3D" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
position = Vector3(0, 1, -0.3)
|
position = Vector3(0, 1, -0.3)
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
collision_mask = 2
|
collision_mask = 2
|
||||||
script = ExtResource("1_shop")
|
script = ExtResource("1_shop")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="FishingShopInteraction"]
|
[node name="InteractionShape" type="CollisionShape3D" parent="InteractionArea"]
|
||||||
shape = SubResource("InteractionShape")
|
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"]
|
[sub_resource type="BoxMesh" id="PerchMesh"]
|
||||||
|
resource_local_to_scene = true
|
||||||
size = Vector3(5, 0.7, 2.2)
|
size = Vector3(5, 0.7, 2.2)
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="PostMesh"]
|
[sub_resource type="CylinderMesh" id="PostMesh"]
|
||||||
|
resource_local_to_scene = true
|
||||||
top_radius = 0.18
|
top_radius = 0.18
|
||||||
bottom_radius = 0.24
|
bottom_radius = 0.24
|
||||||
height = 2.5
|
height = 2.5
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="WoodMaterial"]
|
[sub_resource type="StandardMaterial3D" id="WoodMaterial"]
|
||||||
|
resource_local_to_scene = true
|
||||||
albedo_color = Color(0.46, 0.28, 0.13, 1)
|
albedo_color = Color(0.46, 0.28, 0.13, 1)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="SignMaterial"]
|
[sub_resource type="StandardMaterial3D" id="SignMaterial"]
|
||||||
|
resource_local_to_scene = true
|
||||||
albedo_color = Color(0.95, 0.82, 0.42, 1)
|
albedo_color = Color(0.95, 0.82, 0.42, 1)
|
||||||
|
|
||||||
[node name="PelicanCoolerPerch" type="Node3D"]
|
[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)
|
position = Vector3(0, 1.65, 0)
|
||||||
mesh = SubResource("PerchMesh")
|
mesh = SubResource("PerchMesh")
|
||||||
material_override = SubResource("WoodMaterial")
|
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)
|
position = Vector3(-1.8, 0.7, 0)
|
||||||
mesh = SubResource("PostMesh")
|
mesh = SubResource("PostMesh")
|
||||||
material_override = SubResource("WoodMaterial")
|
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)
|
position = Vector3(1.8, 0.7, 0)
|
||||||
mesh = SubResource("PostMesh")
|
mesh = SubResource("PostMesh")
|
||||||
material_override = SubResource("WoodMaterial")
|
material_override = SubResource("WoodMaterial")
|
||||||
|
|
||||||
[node name="Sign" type="Label3D" parent="Visuals"]
|
[node name="Sign" type="Label3D" parent="Visual"]
|
||||||
position = Vector3(0, 2.55, 0)
|
position = Vector3(0, 2.55, 0)
|
||||||
text = "sell to pelicans\nfrom the cooler"
|
text = "sell to pelicans\nfrom the cooler"
|
||||||
font_size = 30
|
font_size = 30
|
||||||
|
|
@ -41,7 +48,7 @@ outline_size = 8
|
||||||
billboard = 1
|
billboard = 1
|
||||||
no_depth_test = true
|
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)
|
position = Vector3(0, 1.65, -1.15)
|
||||||
text = "available anywhere • 0.25x"
|
text = "available anywhere • 0.25x"
|
||||||
font_size = 20
|
font_size = 20
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,24 @@
|
||||||
|
@tool
|
||||||
class_name PlayerWaterTrigger
|
class_name PlayerWaterTrigger
|
||||||
extends Area3D
|
extends Area3D
|
||||||
|
|
||||||
signal recovery_requested(player: Player, surface_height: float)
|
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 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
|
@export_range(0.0, 2.0, 0.05) var entry_depth_threshold: float = 0.35
|
||||||
|
|
||||||
var _tracked_players: Array[Player] = []
|
var _tracked_players: Array[Player] = []
|
||||||
|
|
@ -11,11 +26,15 @@ var _triggered_players: Dictionary[StringName, bool] = {}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
set_physics_process(false)
|
||||||
|
return
|
||||||
body_entered.connect(_on_body_entered)
|
body_entered.connect(_on_body_entered)
|
||||||
body_exited.connect(_on_body_exited)
|
body_exited.connect(_on_body_exited)
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(_delta: float) -> void:
|
func _physics_process(_delta: float) -> void:
|
||||||
|
var active_surface_height: float = get_surface_height()
|
||||||
for player: Player in _tracked_players.duplicate():
|
for player: Player in _tracked_players.duplicate():
|
||||||
if not is_instance_valid(player):
|
if not is_instance_valid(player):
|
||||||
_tracked_players.erase(player)
|
_tracked_players.erase(player)
|
||||||
|
|
@ -25,10 +44,26 @@ func _physics_process(_delta: float) -> void:
|
||||||
continue
|
continue
|
||||||
if (
|
if (
|
||||||
player.get_body_center_position().y
|
player.get_body_center_position().y
|
||||||
<= surface_height - entry_depth_threshold
|
<= active_surface_height - entry_depth_threshold
|
||||||
):
|
):
|
||||||
_triggered_players[player_key] = true
|
_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:
|
func _on_body_entered(body: Node3D) -> void:
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,28 @@
|
||||||
|
@tool
|
||||||
class_name StarterIslandRegion
|
class_name StarterIslandRegion
|
||||||
extends GrayboxRegion
|
extends WorldRegion
|
||||||
|
|
||||||
const FishingShopInteractionType = preload(
|
const FishingShopInteractionType = preload(
|
||||||
"res://world/fishing_shop_interaction.gd"
|
"res://world/fishing_shop_interaction.gd"
|
||||||
)
|
)
|
||||||
|
|
||||||
@export var visual_mesh_path: NodePath = (
|
@export_group("Owned Nodes")
|
||||||
^"Visuals/StarterIslandModel/starter_island"
|
@export_node_path("MeshInstance3D")
|
||||||
|
var visual_mesh_path: NodePath = (
|
||||||
|
^"Terrain/Visual/starter_island"
|
||||||
)
|
)
|
||||||
@export var terrain_collision_shape_path: NodePath = (
|
@export_node_path("CollisionShape3D")
|
||||||
^"StaticCollision/Terrain/Shape"
|
var terrain_collision_shape_path: NodePath = (
|
||||||
|
^"Terrain/Collision/Shape"
|
||||||
)
|
)
|
||||||
@export var player_spawn_path: NodePath = ^"PlayerSpawn"
|
@export_node_path("Marker3D")
|
||||||
@export var fishing_shop_path: NodePath = (
|
var player_spawn_path: NodePath = ^"PlayerSpawn"
|
||||||
^"Interactables/FishingShopWorld/FishingShopInteraction"
|
@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"
|
^"Interactables/PelicanCoolerPerch"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,6 +34,8 @@ const FishingShopInteractionType = preload(
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_apply_grass_material()
|
_apply_grass_material()
|
||||||
_build_terrain_collision()
|
_build_terrain_collision()
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
update_configuration_warnings()
|
||||||
|
|
||||||
|
|
||||||
func get_player_spawn_transform() -> Transform3D:
|
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.")
|
push_error("Starter island terrain collision could not be created.")
|
||||||
return
|
return
|
||||||
collision_shape.shape = terrain_shape
|
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]:
|
func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
||||||
var 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_array(region.get_water_recovery_triggers())
|
||||||
triggers.append(_below_world_failsafe)
|
triggers.append(_below_world_failsafe)
|
||||||
return triggers
|
return triggers
|
||||||
|
|
@ -27,7 +27,7 @@ func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
||||||
|
|
||||||
func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
|
func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
|
||||||
var 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())
|
points.append_array(region.get_safe_respawn_points())
|
||||||
return points
|
return points
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ func get_player_spawn_transform() -> Transform3D:
|
||||||
|
|
||||||
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
||||||
var waters: 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())
|
waters.append_array(region.get_fishable_water_regions())
|
||||||
return waters
|
return waters
|
||||||
|
|
||||||
|
|
@ -51,10 +51,10 @@ func get_pelican_convenience_landmark() -> Node3D:
|
||||||
return _starter_island.get_pelican_landmark()
|
return _starter_island.get_pelican_landmark()
|
||||||
|
|
||||||
|
|
||||||
func _get_regions() -> Array[GrayboxRegion]:
|
func _get_regions() -> Array[WorldRegion]:
|
||||||
var regions: Array[GrayboxRegion] = []
|
var regions: Array[WorldRegion] = []
|
||||||
for child: Node in _regions_root.get_children():
|
for child: Node in _regions_root.get_children():
|
||||||
var region: GrayboxRegion = child as GrayboxRegion
|
var region: WorldRegion = child as WorldRegion
|
||||||
if region != null:
|
if region != null:
|
||||||
regions.append(region)
|
regions.append(region)
|
||||||
return regions
|
return regions
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://world/test_world.gd" id="1_world"]
|
[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="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"]
|
[sub_resource type="Environment" id="Environment"]
|
||||||
background_mode = 1
|
background_mode = 1
|
||||||
|
|
@ -60,7 +60,7 @@ collision_mask = 2
|
||||||
script = ExtResource("3_water_trigger")
|
script = ExtResource("3_water_trigger")
|
||||||
surface_height = 0.3
|
surface_height = 0.3
|
||||||
|
|
||||||
[node name="FailsafeShape" type="CollisionShape3D" parent="Safety/BelowWorldFailsafe"]
|
[node name="Coverage" type="CollisionShape3D" parent="Safety/BelowWorldFailsafe"]
|
||||||
shape = SubResource("FailsafeShape")
|
shape = SubResource("FailsafeShape")
|
||||||
|
|
||||||
[node name="WorldBounds" type="Node3D" parent="."]
|
[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