e7299b17e9
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
154 lines
5.1 KiB
GDScript
154 lines
5.1 KiB
GDScript
@tool
|
||
extends EditorScript
|
||
|
||
# Apply distance-based visibility ranges to all modular CSG pieces.
|
||
#
|
||
# This is the equivalent of LOD for CSG-based geometry — it hides
|
||
# entire pieces when they're beyond a useful visual distance.
|
||
#
|
||
# Ranges (defined per asset type):
|
||
# Walls: hide past 50 m
|
||
# Floors: hide past 60 m
|
||
# Pillars: hide past 30 m
|
||
# Beams: hide past 30 m
|
||
# Accent panels: hide past 20 m
|
||
# Door/Window: hide past 40 m
|
||
#
|
||
# Usage:
|
||
# Select a scene in the Godot editor, then:
|
||
# Scene > Run Script (select this file)
|
||
#
|
||
# Or from CLI on a specific scene:
|
||
# godot --script scripts/apply_visibility_ranges.gd \
|
||
# --scene res://assets/scenes/modular/kit_demo.tscn
|
||
|
||
const DEBUG := false # Enable for verbose logging
|
||
|
||
# Default ranges per type (begin = 0.0 means always visible up to end)
|
||
const RANGES := {
|
||
"wall": {"begin": 0.0, "end": 50.0, "margin": 5.0}, # fade out over 5m
|
||
"floor": {"begin": 0.0, "end": 60.0, "margin": 10.0},
|
||
"pillar": {"begin": 0.0, "end": 30.0, "margin": 3.0},
|
||
"beam": {"begin": 0.0, "end": 30.0, "margin": 3.0},
|
||
"accent": {"begin": 0.0, "end": 20.0, "margin": 2.0},
|
||
"doorway": {"begin": 0.0, "end": 40.0, "margin": 5.0},
|
||
"window": {"begin": 0.0, "end": 40.0, "margin": 5.0},
|
||
"endcap": {"begin": 0.0, "end": 50.0, "margin": 5.0},
|
||
"default": {"begin": 0.0, "end": 40.0, "margin": 5.0},
|
||
}
|
||
|
||
func _run():
|
||
var scene_root: Node
|
||
|
||
if Engine.is_editor_hint():
|
||
scene_root = get_scene() as Node
|
||
if not scene_root:
|
||
printerr("No open scene in editor. Open a scene first.")
|
||
return
|
||
else:
|
||
var args = OS.get_cmdline_args()
|
||
var scene_path := ""
|
||
for i in range(args.size()):
|
||
if args[i] == "--scene" and i + 1 < args.size():
|
||
scene_path = args[i + 1]
|
||
if scene_path.is_empty():
|
||
printerr("Usage: godot --script apply_visibility_ranges.gd --scene res://path/to/scene.tscn" +
|
||
"\nOr run from the Godot editor with a scene open.")
|
||
return
|
||
|
||
var packed = ResourceLoader.load(scene_path)
|
||
if not packed:
|
||
printerr("Failed to load scene: ", scene_path)
|
||
return
|
||
scene_root = packed.instantiate()
|
||
|
||
print("=== Visibility Range Applicator ===")
|
||
print("Scene: ", scene_root.name)
|
||
|
||
var counts := {} # type → number of nodes modified
|
||
var total := 0
|
||
|
||
_apply_to_node(scene_root, counts, total)
|
||
|
||
print("")
|
||
print("Modified nodes: ", total)
|
||
for type_name in counts:
|
||
print(" %s: %d" % [type_name, counts[type_name]])
|
||
print("")
|
||
print("=== Ranges applied ===")
|
||
|
||
func _apply_to_node(node: Node, counts: Dictionary, total: int) -> void:
|
||
# Only process CSG nodes (the modular pieces)
|
||
if node is CSGShape3D:
|
||
var range_config := _get_range_for_node(node)
|
||
if range_config != null:
|
||
node.visibility_range_begin = range_config.begin
|
||
node.visibility_range_end = range_config.end
|
||
node.visibility_range_begin_margin = range_config.margin
|
||
node.visibility_range_end_margin = range_config.margin
|
||
|
||
var type_name := _classify_node(node)
|
||
counts[type_name] = counts.get(type_name, 0) + 1
|
||
total += 1
|
||
|
||
if DEBUG:
|
||
print(" %s → %.0f–%.0f m (type: %s)" % [node.name, range_config.begin, range_config.end, type_name])
|
||
|
||
# Also apply to CSGCombiner3D parents (hold doorway/window subtractions)
|
||
if node is CSGCombiner3D:
|
||
var range_config := _get_range_for_combiner(node)
|
||
if range_config != null:
|
||
node.visibility_range_begin = range_config.begin
|
||
node.visibility_range_end = range_config.end
|
||
node.visibility_range_begin_margin = range_config.margin
|
||
node.visibility_range_end_margin = range_config.margin
|
||
|
||
var type_name := _classify_node(node)
|
||
counts[type_name] = counts.get(type_name, 0) + 1
|
||
total += 1
|
||
|
||
if DEBUG:
|
||
print(" %s → %.0f–%.0f m (type: %s)" % [node.name, range_config.begin, range_config.end, type_name])
|
||
|
||
# Recurse
|
||
for child in node.get_children():
|
||
_apply_to_node(child, counts, total)
|
||
|
||
func _classify_node(node: Node) -> String:
|
||
var name_lower := node.name.to_lower()
|
||
|
||
if name_lower.contains("wall"):
|
||
if name_lower.contains("door"): return "doorway"
|
||
if name_lower.contains("window"): return "window"
|
||
if name_lower.contains("corner"): return "wall"
|
||
if name_lower.contains("endcap"): return "endcap"
|
||
return "wall"
|
||
if name_lower.contains("floor"): return "floor"
|
||
if name_lower.contains("pillar"): return "pillar"
|
||
if name_lower.contains("beam"): return "beam"
|
||
if name_lower.contains("accent") or name_lower.contains("blue") or name_lower.contains("red"):
|
||
return "accent"
|
||
if name_lower.contains("panel"):
|
||
return "accent"
|
||
|
||
return "default"
|
||
|
||
func _get_range_for_node(node: CSGShape3D) -> Dictionary:
|
||
# Detect CSGBox3D size to infer type
|
||
if node is CSGBox3D:
|
||
var box := node as CSGBox3D
|
||
var size := box.size
|
||
|
||
# Floor slabs: thin, wide (Z > X on rotated ones, check min dimension)
|
||
if size.y <= 0.1 and (size.x >= 2.0 or size.z >= 2.0):
|
||
return RANGES.floor.duplicate()
|
||
|
||
return _get_range_by_name(node)
|
||
|
||
func _get_range_for_combiner(node: CSGCombiner3D) -> Dictionary:
|
||
return _get_range_by_name(node)
|
||
|
||
func _get_range_by_name(node: Node) -> Dictionary:
|
||
var type_name := _classify_node(node)
|
||
return RANGES.get(type_name, RANGES.default).duplicate()
|