Three compounding issues, found via user testing: 1. Sprite was 2.56x2.56 units (raw 256px checkerboard at default pixel_size) — 5x wider than the player, and collision shapes sized to roughly match. Gave AnimatedSprite3D an explicit pixel_size (0.004) for a ~1 unit sprite, shrunk the solid CylinderShape3D to radius 0.4/height 1.0 and the Area3D trigger sphere to radius 0.6 accordingly (still >= the solid radius, so touching = interactable holds). 2. InteractionPrompt had zero vertical offset, sitting dead center of the (huge) sprite — rendering behind/inside it, so the "[F] Interact" hint never appeared even though proximity detection and the keypress itself worked (confirmed by the user: pressing F did work, the hint just never showed telling them to). Gave both NameLabel and InteractionPrompt proper offsets above the sprite. 3. Once the collision shapes shrank, a pre-existing ~0.7 unit gap between each POI's hand-placed Y (computed with the same radius-only height approximation that's only exact along a facet's straight edges, not its interior — same class of error as the earlier Ort6 out-of-bounds bug) and the true terrain surface became consequential: the small collider mostly floated above the real ground, giving wildly direction-dependent, sometimes entirely missing contact/interaction depending on approach angle. Root-fixed rather than re-deriving 36+ positions' exact heights by hand again: poi.gd/poi_gate.gd now raycast straight down against the terrain in _ready() (after one physics_frame, so the terrain's collision shape has had time to register) and snap themselves onto the real surface plus a small clearance. Verified headless: approaching a POI from 4 different directions now gives consistent ~0.56 unit contact distance and player_nearby=true in all four (previously: two directions barely touched at ~0.05 units with interaction never triggering, the other two behaved correctly — direction-dependent failure, now gone). Re-ran the full 6-gate chain (still passes) and gate-to-border-prop clearance across 3 randomized layouts (still 1.3-1.9 units clear, no regression).
84 lines
2.7 KiB
GDScript
84 lines
2.7 KiB
GDScript
extends Area3D
|
|
|
|
@export var poi_name: String = "Tor zur Mitte"
|
|
@export var locked_name: String = "Verschlossen"
|
|
@export var marker_color: Color = Color(1.0, 0.9, 0.4)
|
|
@export var required_group: String = "region1_pois"
|
|
@export var target_position: Vector3 = Vector3.ZERO
|
|
|
|
@onready var sprite: AnimatedSprite3D = $AnimatedSprite3D
|
|
@onready var name_label: Label3D = $Billboard/NameLabel
|
|
@onready var prompt: Label3D = $Billboard/InteractionPrompt
|
|
|
|
var player_nearby: bool = false
|
|
|
|
func _ready() -> void:
|
|
prompt.visible = false
|
|
prompt.text = "[F] Interact"
|
|
_update_visuals()
|
|
body_entered.connect(_on_body_entered)
|
|
body_exited.connect(_on_body_exited)
|
|
# Terrain-Kollision braucht mind. einen Physik-Tick um registriert zu sein
|
|
await get_tree().physics_frame
|
|
_snap_to_terrain()
|
|
|
|
# Siehe poi.gd: handplatzierte Y-Werte treffen die geneigten Facetten nicht
|
|
# exakt, per Raycast auf die echte Terrain-Oberfläche schnappen.
|
|
func _snap_to_terrain() -> void:
|
|
var space_state := get_world_3d().direct_space_state
|
|
var from := global_position + Vector3(0, 20, 0)
|
|
var to := global_position + Vector3(0, -20, 0)
|
|
var query := PhysicsRayQueryParameters3D.create(from, to)
|
|
query.exclude = [get_rid(), $StaticBody3D.get_rid()]
|
|
var result := space_state.intersect_ray(query)
|
|
if result:
|
|
global_position.y = result.position.y + 0.3
|
|
|
|
func _all_required_visited() -> bool:
|
|
var pois := get_tree().get_nodes_in_group(required_group)
|
|
if pois.is_empty():
|
|
return false
|
|
for poi in pois:
|
|
if not poi.visited:
|
|
return false
|
|
return true
|
|
|
|
func _update_visuals() -> void:
|
|
if _all_required_visited():
|
|
sprite.modulate = marker_color
|
|
name_label.text = poi_name
|
|
else:
|
|
sprite.modulate = marker_color * Color(0.35, 0.35, 0.35, 1.0)
|
|
name_label.text = locked_name
|
|
|
|
func _on_body_entered(body: Node3D) -> void:
|
|
if body.is_in_group("player"):
|
|
player_nearby = true
|
|
_update_visuals()
|
|
# Verschlossen: nicht ansprechbar, kein Prompt
|
|
prompt.visible = _all_required_visited()
|
|
|
|
func _on_body_exited(body: Node3D) -> void:
|
|
if body.is_in_group("player"):
|
|
player_nearby = false
|
|
prompt.visible = false
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if player_nearby and _all_required_visited() and Input.is_action_just_pressed("interact"):
|
|
InteractionManager.active = true
|
|
InteractionManager.current_poi = self
|
|
InteractionUI.show_menu("Durchgehen?", [
|
|
{ "label": "Ja", "action": "gate_yes" },
|
|
{ "label": "Nein", "action": "gate_no" },
|
|
])
|
|
|
|
func handle_interaction_action(action: String) -> void:
|
|
match action:
|
|
"gate_yes":
|
|
var player := get_tree().get_first_node_in_group("player")
|
|
if player:
|
|
player.global_position = target_position
|
|
InteractionManager.close()
|
|
"gate_no":
|
|
InteractionManager.close()
|