Files
modbison/scripts/static_object_node.gd
T
2026-09-17 23:35:33 +00:00

392 lines
11 KiB
GDScript

class_name StaticObjectNode
extends Node3D
## A visual representation of a static world object within a chunk.
##
## This node is instantiated by [ChunkScene] for each static (non-dynamic)
## [WorldObject]. It renders the object's mesh (referenced by `asset_id`) at
## the object's transform. If the asset is not yet loaded, a placeholder mesh
## is shown.
##
## ## Structure
##
## ```
## StaticObjectNode (Node3D)
## └── MeshInstance3D # the object's visual mesh
## ```
const ScriptBehaviorClass := preload("res://scripts/scripting/script_behavior.gd")
const ScriptInterpreterNodeClass := preload("res://scripts/scripting/script_interpreter.gd")
## The world object data this node represents.
var world_object: WorldObject = null
## The loaded mesh instance (or placeholder).
var _mesh_instance: MeshInstance3D = null
func _ready() -> void:
if world_object == null:
return
_load_mesh()
_setup_behavior()
func _setup_behavior() -> void:
if world_object == null or not world_object.properties is Dictionary:
return
var b_dict = world_object.properties.get("script_behavior", world_object.properties.get("behavior"))
if b_dict is Dictionary and not b_dict.is_empty():
var behavior := ScriptBehaviorClass.load_from_dict(b_dict)
if behavior != null and not behavior.rules.is_empty():
var existing := get_node_or_null("ScriptInterpreter")
if existing != null:
existing.queue_free()
var interp := ScriptInterpreterNodeClass.new()
interp.name = "ScriptInterpreter"
interp.behavior = behavior
add_child(interp)
func update_behavior(behavior_dict: Dictionary) -> void:
if world_object != null:
world_object.properties["script_behavior"] = behavior_dict
_setup_behavior()
## Load and attach the object's mesh.
func _load_mesh() -> void:
# 1. Try to load the asset directly if it exists on disk
var asset_node := _try_load_asset(world_object.asset_id)
if asset_node != null:
add_child(asset_node)
if asset_node is MeshInstance3D:
_mesh_instance = asset_node as MeshInstance3D
return
# 2. Build a stylized, recognizable 3D representation based on catalog ID / asset ID
var cat_id := _get_catalog_id()
_build_procedural_object(cat_id)
func _get_catalog_id() -> String:
if world_object == null:
return ""
if world_object.properties is Dictionary and world_object.properties.has("catalog_id"):
return str(world_object.properties["catalog_id"]).to_lower()
return world_object.asset_id.get_file().get_basename().to_lower()
func _try_load_asset(asset_id: String) -> Node:
if asset_id.is_empty():
return null
# 1. Try standard project asset path (res://assets/...)
var path := "res://assets/%s" % asset_id
if ResourceLoader.exists(path):
var res := load(path)
if res is PackedScene:
return (res as PackedScene).instantiate()
elif res is Mesh:
var mi := MeshInstance3D.new()
mi.name = "mesh"
mi.mesh = res as Mesh
return mi
# 2. Try direct path or custom asset path
var candidate_paths: Array[String] = [
asset_id,
"res://assets/%s" % asset_id,
"user://custom_assets/%s" % asset_id,
"user://custom_assets/%s" % asset_id.get_file()
]
for cpath in candidate_paths:
if FileAccess.file_exists(cpath):
if cpath.ends_with(".glb") or cpath.ends_with(".gltf"):
var gltf := GLTFDocument.new()
var state := GLTFState.new()
var err := gltf.append_from_file(cpath, state)
if err == OK:
var scene := gltf.generate_scene(state)
if scene != null:
return scene
elif ResourceLoader.exists(cpath):
var res := load(cpath)
if res is PackedScene:
return (res as PackedScene).instantiate()
elif res is Mesh:
var mi := MeshInstance3D.new()
mi.name = "mesh"
mi.mesh = res as Mesh
return mi
return null
func _build_procedural_object(cat_id: String) -> void:
var container := Node3D.new()
container.name = "visuals"
add_child(container)
if "lamp" in cat_id:
# Lamp post: dark iron vertical pole + glowing lantern + omni light
var pole := MeshInstance3D.new()
pole.name = "pole"
var pole_mesh := CylinderMesh.new()
pole_mesh.height = 3.2
pole_mesh.top_radius = 0.07
pole_mesh.bottom_radius = 0.1
var iron_mat := StandardMaterial3D.new()
iron_mat.albedo_color = Color(0.2, 0.22, 0.25)
iron_mat.metallic = 0.85
iron_mat.roughness = 0.35
pole_mesh.material = iron_mat
pole.mesh = pole_mesh
pole.position = Vector3(0, 1.6, 0)
container.add_child(pole)
_mesh_instance = pole
var cap := MeshInstance3D.new()
cap.name = "cap"
var cap_mesh := CylinderMesh.new()
cap_mesh.height = 0.15
cap_mesh.top_radius = 0.05
cap_mesh.bottom_radius = 0.3
cap_mesh.material = iron_mat
cap.mesh = cap_mesh
cap.position = Vector3(0, 3.2, 0)
container.add_child(cap)
var lantern := MeshInstance3D.new()
lantern.name = "lantern"
var lantern_mesh := BoxMesh.new()
lantern_mesh.size = Vector3(0.32, 0.45, 0.32)
var glow_mat := StandardMaterial3D.new()
glow_mat.albedo_color = Color(1.0, 0.95, 0.7)
glow_mat.emission_enabled = true
glow_mat.emission = Color(1.0, 0.9, 0.4)
glow_mat.emission_energy_multiplier = 4.0
lantern_mesh.material = glow_mat
lantern.mesh = lantern_mesh
lantern.position = Vector3(0, 2.9, 0)
container.add_child(lantern)
var light := OmniLight3D.new()
light.name = "light"
light.light_color = Color(1.0, 0.93, 0.72)
light.light_energy = 1.6
light.omni_range = 10.0
light.position = Vector3(0, 2.9, 0)
container.add_child(light)
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var cyl := CylinderShape3D.new()
cyl.height = 3.2
cyl.radius = 0.2
col_shape.shape = cyl
col_shape.position = Vector3(0, 1.6, 0)
col.add_child(col_shape)
container.add_child(col)
elif "crate" in cat_id:
var crate := MeshInstance3D.new()
crate.name = "crate"
var crate_mesh := BoxMesh.new()
crate_mesh.size = Vector3(1.0, 1.0, 1.0)
var wood_mat := StandardMaterial3D.new()
wood_mat.albedo_color = Color(0.55, 0.36, 0.18)
wood_mat.roughness = 0.8
crate_mesh.material = wood_mat
crate.mesh = crate_mesh
crate.position = Vector3(0, 0.5, 0)
container.add_child(crate)
_mesh_instance = crate
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(1.0, 1.0, 1.0)
col_shape.shape = box
col_shape.position = Vector3(0, 0.5, 0)
col.add_child(col_shape)
container.add_child(col)
elif "rock" in cat_id:
var rock := MeshInstance3D.new()
rock.name = "rock"
var rock_mesh := SphereMesh.new()
rock_mesh.radius = 0.65
rock_mesh.height = 1.0
rock_mesh.radial_segments = 8
rock_mesh.rings = 6
var stone_mat := StandardMaterial3D.new()
stone_mat.albedo_color = Color(0.42, 0.44, 0.46)
stone_mat.roughness = 0.9
rock_mesh.material = stone_mat
rock.mesh = rock_mesh
rock.position = Vector3(0, 0.5, 0)
container.add_child(rock)
_mesh_instance = rock
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var sphere := SphereShape3D.new()
sphere.radius = 0.65
col_shape.shape = sphere
col_shape.position = Vector3(0, 0.5, 0)
col.add_child(col_shape)
container.add_child(col)
elif "tree" in cat_id:
var trunk := MeshInstance3D.new()
trunk.name = "trunk"
var trunk_mesh := CylinderMesh.new()
trunk_mesh.height = 1.4
trunk_mesh.top_radius = 0.2
trunk_mesh.bottom_radius = 0.25
var bark_mat := StandardMaterial3D.new()
bark_mat.albedo_color = Color(0.35, 0.22, 0.12)
trunk_mesh.material = bark_mat
trunk.mesh = trunk_mesh
trunk.position = Vector3(0, 0.7, 0)
container.add_child(trunk)
var leaves1 := MeshInstance3D.new()
leaves1.name = "leaves1"
var leaves1_mesh := CylinderMesh.new()
leaves1_mesh.height = 2.0
leaves1_mesh.top_radius = 0.1
leaves1_mesh.bottom_radius = 1.4
var leaves_mat := StandardMaterial3D.new()
leaves_mat.albedo_color = Color(0.12, 0.38, 0.15)
leaves1_mesh.material = leaves_mat
leaves1.mesh = leaves1_mesh
leaves1.position = Vector3(0, 2.0, 0)
container.add_child(leaves1)
_mesh_instance = leaves1
var leaves2 := MeshInstance3D.new()
leaves2.name = "leaves2"
var leaves2_mesh := CylinderMesh.new()
leaves2_mesh.height = 1.6
leaves2_mesh.top_radius = 0.0
leaves2_mesh.bottom_radius = 0.9
leaves2_mesh.material = leaves_mat
leaves2.mesh = leaves2_mesh
leaves2.position = Vector3(0, 3.2, 0)
container.add_child(leaves2)
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var cyl := CylinderShape3D.new()
cyl.height = 4.0
cyl.radius = 0.35
col_shape.shape = cyl
col_shape.position = Vector3(0, 2.0, 0)
col.add_child(col_shape)
container.add_child(col)
elif "chest" in cat_id:
var chest := MeshInstance3D.new()
chest.name = "chest"
var chest_mesh := BoxMesh.new()
chest_mesh.size = Vector3(1.0, 0.8, 0.6)
var chest_mat := StandardMaterial3D.new()
chest_mat.albedo_color = Color(0.48, 0.26, 0.12)
chest_mesh.material = chest_mat
chest.mesh = chest_mesh
chest.position = Vector3(0, 0.4, 0)
container.add_child(chest)
_mesh_instance = chest
var trim := MeshInstance3D.new()
trim.name = "trim"
var trim_mesh := BoxMesh.new()
trim_mesh.size = Vector3(0.2, 0.25, 0.64)
var gold_mat := StandardMaterial3D.new()
gold_mat.albedo_color = Color(0.9, 0.75, 0.2)
trim_mesh.material = gold_mat
trim.mesh = trim_mesh
trim.position = Vector3(0, 0.4, 0)
container.add_child(trim)
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(1.0, 0.8, 0.6)
col_shape.shape = box
col_shape.position = Vector3(0, 0.4, 0)
col.add_child(col_shape)
container.add_child(col)
elif "spawn" in cat_id or (world_object != null and world_object.type == "spawn_point"):
var pad := MeshInstance3D.new()
pad.name = "pad"
var pad_mesh := CylinderMesh.new()
pad_mesh.height = 0.08
pad_mesh.top_radius = 1.2
pad_mesh.bottom_radius = 1.2
var spawn_mat := StandardMaterial3D.new()
spawn_mat.albedo_color = Color(0.2, 0.75, 0.95)
spawn_mat.emission_enabled = true
spawn_mat.emission = Color(0.1, 0.6, 0.85)
spawn_mat.emission_energy_multiplier = 2.0
pad_mesh.material = spawn_mat
pad.mesh = pad_mesh
pad.position = Vector3(0, 0.04, 0)
container.add_child(pad)
_mesh_instance = pad
else:
var mi := MeshInstance3D.new()
mi.name = "mesh"
mi.mesh = _make_placeholder_mesh()
mi.position = Vector3(0, 0.5, 0)
container.add_child(mi)
_mesh_instance = mi
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(1.0, 1.0, 1.0)
col_shape.shape = box
col_shape.position = Vector3(0, 0.5, 0)
col.add_child(col_shape)
container.add_child(col)
func _create_static_collision() -> StaticBody3D:
var sb := StaticBody3D.new()
sb.name = "collision"
sb.set_collision_layer_value(1, true)
sb.set_collision_mask_value(1, true)
return sb
## Attempt to load a mesh from the asset path.
func _load_asset_mesh(asset_id: String) -> Mesh:
if asset_id.is_empty():
return null
var path := "res://assets/%s" % asset_id
if ResourceLoader.exists(path):
var res := load(path)
if res is Mesh:
return res as Mesh
return null
## Create a simple placeholder box mesh when the real asset is unavailable.
func _make_placeholder_mesh() -> Mesh:
var mesh := BoxMesh.new()
mesh.size = Vector3(1.0, 1.0, 1.0)
return mesh
## Get the world object's display name.
func get_display_name() -> String:
if world_object == null:
return ""
return world_object.get_display_name()