Explosion_der_Spiele/scripts/levels/village.gd

651 lines
21 KiB
GDScript3
Raw Permalink Normal View History

@tool
extends Node3D
## Hauptwelt: Dorf + Zaun/Tor + Wiese nebeneinander. Knopf → Reif vor Ort → Tor öffnet → Wiese zu Fuß.
## @tool: editor_preview_spawns shows deterministic ModelProp stand-ins at slot positions
## (never owned/saved; runtime clears them before random spawn).
const HOUSES_DIR := "res://assets/3DModelle/houses"
const ANIMALS_DIR := "res://assets/3DModelle/animals"
const MASSEUR_HOUSE_MODEL := "res://assets/3DModelle/houses/massour/massourhaus.glb"
const COW_MODEL := "res://assets/3DModelle/animals/cow/cow.glb"
const MODEL_PROP_SCENE := "res://scenes/prefabs/model_prop.tscn"
const ENEMY_SCENE := "res://scenes/prefabs/enemy.tscn"
const HOUSE_POOL_SKIP := ["massour"]
const ANIMAL_POOL_SKIP := ["cow"] ## cow = Masseur-Kuh only
const EDITOR_PREVIEW_PREFIX := "_EditorPreview_"
## Heights from data/model_scales.json via SpriteFit (scale lab SSOT).
const HOUSE_SLOTS: Array[Dictionary] = [
{"pos": Vector3(7.0, 0.0, -6.0), "yaw": 15.0, "mul": 1.0},
{"pos": Vector3(-9.0, 0.0, -7.0), "yaw": 40.0, "mul": 0.95},
{"pos": Vector3(12.0, 0.0, -11.0), "yaw": -25.0, "mul": 0.9},
{"pos": Vector3(-4.0, 0.0, -14.0), "yaw": 85.0, "mul": 1.05},
{"pos": Vector3(14.0, 0.0, -3.0), "yaw": -50.0, "mul": 0.92},
{"pos": Vector3(-14.0, 0.0, -12.0), "yaw": 130.0, "mul": 0.88},
]
const ANIMAL_SLOTS: Array[Dictionary] = [
{"pos": Vector3(2.5, 0.0, 2.0), "yaw": 40.0, "mul": 1.0},
{"pos": Vector3(-2.0, 0.0, -1.5), "yaw": -70.0, "mul": 0.9},
{"pos": Vector3(6.5, 0.0, 0.5), "yaw": 15.0, "mul": 0.85},
{"pos": Vector3(-8.0, 0.0, 1.0), "yaw": 110.0, "mul": 1.05},
{"pos": Vector3(1.0, 0.0, -6.0), "yaw": -30.0, "mul": 1.1},
{"pos": Vector3(-4.5, 0.0, -5.5), "yaw": 200.0, "mul": 0.8},
{"pos": Vector3(9.0, 0.0, -4.0), "yaw": -100.0, "mul": 0.75},
]
## When on (editor only): spawn representative houses/animals at slot positions.
@export var editor_preview_spawns: bool = true:
set(v):
editor_preview_spawns = v
if Engine.is_editor_hint() and is_node_ready():
_editor_refresh_preview()
@onready var choice_ui: CanvasLayer = $ChoiceUI
var _flee_dirs: Dictionary = {}
var _throw_cd: float = 1.2
var _reif_alive: bool = false
var _reif_fight_active: bool = false
var _gate_open: bool = false
func _ready() -> void:
if Engine.is_editor_hint():
_editor_refresh_preview()
set_process(false)
set_physics_process(false)
return
## Drop any leftover editor stand-ins before runtime spawn (no double houses).
_free_editor_previews()
GameState.current_scene_id = "village"
GameState.heal_full()
_clear_legacy_house_nodes()
_spawn_random_houses()
_spawn_random_animals()
_spawn_masseur_house()
_spawn_cow_masseur()
_spawn_progress_house()
_setup_table_button()
_setup_reif()
_setup_gate()
if choice_ui:
choice_ui.visible = false
# Opening bark must not hard-block entry (blocking VN + player mouse capture race).
var ui := get_node_or_null("/root/UIRoot")
if ui and ui.has_method("show_dialog_key"):
if Inventory.has_item("snake") and not GameState.get_flag("snake_massaged", false):
ui.show_dialog_key("dialog_villager_snake", 3.5, false)
else:
ui.show_dialog_key("dialog_villager_run", 3.5, false)
var dentist := get_node_or_null("Dentist")
if dentist:
dentist.give_item_id = ""
dentist.achievement_id = ""
dentist.quest_flag = ""
dentist.dialog_key = "dialog_villager_snake"
var fan := get_node_or_null("NPC_HongkongFan")
if fan:
fan.give_item_id = ""
fan.dialog_key = "dialog_villager_run"
_setup_flee()
func _notification(what: int) -> void:
if what == NOTIFICATION_EDITOR_PRE_SAVE:
## Belt-and-suspenders: never persist preview meshes into village.tscn.
_strip_preview_owners()
func _free_editor_previews() -> void:
var to_free: Array[Node] = []
for child in get_children():
if str(child.name).begins_with(EDITOR_PREVIEW_PREFIX):
to_free.append(child)
for n in to_free:
remove_child(n)
n.free()
func _strip_preview_owners() -> void:
for child in get_children():
if str(child.name).begins_with(EDITOR_PREVIEW_PREFIX):
_clear_owner_recursive(child)
func _clear_owner_recursive(n: Node) -> void:
n.owner = null
for c in n.get_children():
_clear_owner_recursive(c)
func _editor_refresh_preview() -> void:
if not Engine.is_editor_hint():
return
_free_editor_previews()
if not editor_preview_spawns:
return
## Deterministic pool picks so layout is stable while editing (runtime still random).
var houses := _list_random_house_models()
var animals := _list_random_animal_models()
var i := 0
for slot in HOUSE_SLOTS:
if houses.is_empty():
break
var path: String = houses[i % houses.size()]
_spawn_prop(
"%sRandomHouse_%d" % [EDITOR_PREVIEW_PREFIX, i],
path,
slot["pos"],
float(slot.get("yaw", 0.0)),
SpriteFit.fit_height_for(path, SpriteFit.WORLD_HOUSE_HEIGHT),
SpriteFit.fit_max_width_for(path, SpriteFit.WORLD_HOUSE_MAX_WIDTH),
float(slot.get("mul", 1.0))
)
i += 1
i = 0
for slot in ANIMAL_SLOTS:
if animals.is_empty():
break
var apath: String = animals[i % animals.size()]
_spawn_prop(
"%sRandomAnimal_%d" % [EDITOR_PREVIEW_PREFIX, i],
apath,
slot["pos"],
float(slot.get("yaw", 0.0)),
SpriteFit.animal_height(apath),
SpriteFit.animal_max_width(apath),
float(slot.get("mul", 1.0)),
false,
false
)
i += 1
if ResourceLoader.exists(MASSEUR_HOUSE_MODEL):
_spawn_prop(
"%sMasseurHouse" % EDITOR_PREVIEW_PREFIX,
MASSEUR_HOUSE_MODEL,
Vector3(-5.5, 0.0, 2.5),
-40.0,
SpriteFit.fit_height_for(MASSEUR_HOUSE_MODEL, SpriteFit.WORLD_HOUSE_HEIGHT),
SpriteFit.fit_max_width_for(MASSEUR_HOUSE_MODEL, SpriteFit.WORLD_HOUSE_MAX_WIDTH),
1.0,
false
)
if ResourceLoader.exists(COW_MODEL):
_spawn_prop(
"%sCowMasseur" % EDITOR_PREVIEW_PREFIX,
COW_MODEL,
Vector3(-4.2, 0.0, 3.8),
140.0,
SpriteFit.fit_height_for(COW_MODEL, SpriteFit.WORLD_COW_HEIGHT),
SpriteFit.fit_max_width_for(COW_MODEL, 2.4),
1.0,
true,
false
)
if not houses.is_empty():
var ppath: String = houses[0]
_spawn_prop(
"%sProgressHouse" % EDITOR_PREVIEW_PREFIX,
ppath,
Vector3(0.0, 0.0, -16.0),
0.0,
SpriteFit.fit_height_for(ppath, SpriteFit.WORLD_HOUSE_HEIGHT),
SpriteFit.fit_max_width_for(ppath, SpriteFit.WORLD_HOUSE_MAX_WIDTH),
1.0,
false
)
func _setup_table_button() -> void:
var btn := get_node_or_null("Table/TableButton")
if btn == null:
return
btn.set("on_success_goto", "")
btn.set("can_interact", true)
btn.set("prompt_key", "prompt_press_button")
if btn.has_signal("interacted") and not btn.interacted.is_connected(_on_table_button):
btn.interacted.connect(_on_table_button)
func _on_table_button(_prop: Node = null) -> void:
if GameState.get_flag("reif_defeated", false):
var ui := get_node_or_null("/root/UIRoot")
if ui and ui.has_method("show_dialog_key"):
ui.show_dialog_key("exit_after_reif", 3.5)
return
if _reif_fight_active:
return
_start_reif_fight()
func _setup_reif() -> void:
var reif := get_node_or_null("Reif")
if reif == null:
return
if GameState.get_flag("reif_defeated", false):
reif.queue_free()
_reif_alive = false
_reif_fight_active = false
return
## Dormant until table button — no chase / no interact yet.
reif.visible = false
reif.set("ai_enabled", false)
reif.process_mode = Node.PROCESS_MODE_DISABLED
if reif.is_in_group("interactable"):
reif.remove_from_group("interactable")
if reif.is_in_group("enemy"):
reif.remove_from_group("enemy")
_reif_alive = true
_reif_fight_active = false
func _start_reif_fight() -> void:
var reif := get_node_or_null("Reif")
if reif == null:
reif = _spawn_reif()
if reif == null:
return
_reif_fight_active = true
_reif_alive = true
reif.visible = true
reif.process_mode = Node.PROCESS_MODE_INHERIT
if not reif.is_in_group("interactable"):
reif.add_to_group("interactable")
if not reif.is_in_group("enemy"):
reif.add_to_group("enemy")
reif.set("ai_enabled", true)
reif.set("move_speed", 3.2)
reif.set("attack_damage", 1)
reif.set("attack_cooldown", 1.1)
reif.set("attack_range", 1.25)
reif.set("death_cause_key", "death_shoe")
reif.set("shout_key", "dialog_reif")
reif.set("shout_interval", 5.0)
reif.set("billboard_height", SpriteFit.WORLD_BOSS_HEIGHT)
reif.set("max_hp", 8)
reif.set("hp", 8)
reif.set("defeat_flag", "reif_defeated")
if reif.has_signal("defeated") and not reif.defeated.is_connected(_on_reif_defeated):
reif.defeated.connect(_on_reif_defeated)
var ui := get_node_or_null("/root/UIRoot")
if ui and ui.has_method("show_dialog_key"):
ui.show_dialog_key("dialog_reif")
func _spawn_reif() -> Node3D:
var packed: PackedScene = load(ENEMY_SCENE)
if packed == null:
return null
var reif: Node3D = packed.instantiate()
reif.name = "Reif"
reif.set("max_hp", 8)
reif.set("texture_path", "res://assets/sprites/placeholders/reif.png")
reif.set("billboard_height", SpriteFit.WORLD_BOSS_HEIGHT)
reif.set("defeat_flag", "reif_defeated")
add_child(reif)
reif.global_position = Vector3(0.0, 0.1, -1.5)
return reif
func _physics_process(delta: float) -> void:
if Engine.is_editor_hint():
return
if not _reif_fight_active or not _reif_alive:
return
var reif := get_node_or_null("Reif")
if reif == null or not is_instance_valid(reif):
_reif_alive = false
return
_throw_cd -= delta
if _throw_cd > 0.0:
return
_throw_cd = randf_range(1.6, 2.4)
_throw_shoe(reif as Node3D)
func _throw_shoe(reif: Node3D) -> void:
var player := get_tree().get_first_node_in_group("player")
if player == null or not (player is Node3D):
return
var dist := reif.global_position.distance_to((player as Node3D).global_position)
if dist < 2.0 and randf() > 0.35:
return
var scene: PackedScene = load("res://scenes/prefabs/shoe_projectile.tscn")
if scene == null:
return
var shoe: Node3D = scene.instantiate()
add_child(shoe)
shoe.global_position = reif.global_position + Vector3(0, 1.1, 0)
var target: Vector3 = (player as Node3D).global_position + Vector3(0, 0.9, 0)
var to_target := target - shoe.global_position
var horiz := Vector3(to_target.x, 0.0, to_target.z)
var speed := 9.0
if horiz.length() > 0.01:
horiz = horiz.normalized()
else:
horiz = Vector3(0, 0, 1)
var vy := 3.5 + dist * 0.35
shoe.set("velocity", Vector3(horiz.x * speed, vy, horiz.z * speed))
shoe.set("damage", 1)
var ui := get_node_or_null("/root/UIRoot")
if ui and ui.has_method("show_dialog") and randf() < 0.4:
ui.show_dialog_key("dialog_reif", 1.5, false)
AudioDirector.play_hit()
func _on_reif_defeated() -> void:
_reif_alive = false
_reif_fight_active = false
for child in get_children():
if child.is_in_group("shoe_projectile"):
child.queue_free()
_spawn_tiramisu_tonne()
GameState.set_flag("reif_defeated", true)
_open_gate(true)
var ui := get_node_or_null("/root/UIRoot")
if ui and ui.has_method("show_dialog"):
ui.show_dialog(Locale.tr_key("prompt_eat_tiramisu") + "\n" + Locale.tr_key("exit_after_reif"), 5.0)
func _spawn_tiramisu_tonne() -> void:
if get_node_or_null("TiramisuTonne") != null:
return
var scene: PackedScene = load("res://scenes/prefabs/npc.tscn")
if scene == null:
return
var tonne: Node3D = scene.instantiate()
tonne.name = "TiramisuTonne"
tonne.set("dialog_key", "dialog_tiramisu")
tonne.set("texture_path", "res://assets/sprites/placeholders/tiramisu.png")
tonne.set("billboard_height", 1.8)
tonne.set("billboard_max_width", 2.2)
tonne.set("give_item_id", "")
tonne.set("on_success_goto", "")
tonne.set("quest_flag", "")
add_child(tonne)
tonne.global_position = Vector3(0, 0.1, -2)
var bill = tonne.get_node_or_null("BillboardSprite")
if bill:
if bill.has_method("set_target_height"):
bill.set_target_height(1.8)
bill.set("max_width", 2.2)
if bill.has_method("set_texture"):
var tex: Texture2D = load("res://assets/sprites/placeholders/tiramisu.png")
if tex:
bill.set_texture(tex)
if tonne.has_signal("interacted"):
tonne.interacted.connect(_on_tonne_interacted)
func _on_tonne_interacted(_n: Node = null) -> void:
if GameState.get_flag("tiramisu_eaten", false):
return
GameState.set_flag("tiramisu_eaten", true)
if not Inventory.has_item("tiramisu"):
Inventory.add_item("tiramisu", 1)
GameState.apply_fat(600.0)
var ui := get_node_or_null("/root/UIRoot")
if ui and ui.has_method("show_dialog"):
ui.show_dialog_key("dialog_tiramisu", 5.0)
var tonne := get_node_or_null("TiramisuTonne")
if tonne:
tonne.queue_free()
func _setup_gate() -> void:
var gate := get_node_or_null("MeadowGate")
var label := gate.get_node_or_null("GateLabel") if gate else null
if GameState.get_flag("reif_defeated", false):
_open_gate(false)
elif label is Label3D:
(label as Label3D).text = Locale.tr_key("gate_locked")
func _open_gate(animate: bool) -> void:
_gate_open = true
var gate := get_node_or_null("MeadowGate")
if gate == null:
return
var col := gate.get_node_or_null("CollisionShape3D")
if col is CollisionShape3D:
(col as CollisionShape3D).disabled = true
## Swing open toward meadow (+X).
if animate:
var tw := create_tween()
tw.tween_property(gate, "rotation_degrees:y", -95.0, 0.85).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
else:
gate.rotation_degrees.y = -95.0
var label := gate.get_node_or_null("GateLabel")
if label is Label3D:
(label as Label3D).text = Locale.tr_key("gate_open")
func _scan_glb_pool(root_dir: String, skip_folders: Array) -> Array[String]:
var out: Array[String] = []
var dir := DirAccess.open(root_dir)
if dir == null:
return out
dir.list_dir_begin()
var folder := dir.get_next()
while folder != "":
if dir.current_is_dir() and not folder.begins_with(".") and folder not in skip_folders:
var sub := "%s/%s" % [root_dir, folder]
var subdir := DirAccess.open(sub)
if subdir:
subdir.list_dir_begin()
var fname := subdir.get_next()
while fname != "":
if not subdir.current_is_dir() and fname.to_lower().ends_with(".glb"):
var path := "%s/%s" % [sub, fname]
if ResourceLoader.exists(path):
out.append(path)
fname = subdir.get_next()
folder = dir.get_next()
return out
func _list_random_house_models() -> Array[String]:
return _scan_glb_pool(HOUSES_DIR, HOUSE_POOL_SKIP)
func _list_random_animal_models() -> Array[String]:
return _scan_glb_pool(ANIMALS_DIR, ANIMAL_POOL_SKIP)
func _clear_legacy_house_nodes() -> void:
for child in get_children():
if str(child.name).begins_with("House"):
child.queue_free()
func _spawn_prop(name_: String, path: String, pos: Vector3, yaw: float, fit_h: float, fit_w: float, mul: float, interact: bool = false, collide: bool = true) -> Node3D:
var packed: PackedScene = load(MODEL_PROP_SCENE)
if packed == null:
return null
var prop: Node3D = packed.instantiate()
prop.name = name_
prop.set("model_path", path)
prop.set("fit_height", fit_h)
prop.set("fit_max_width", fit_w)
prop.set("ground_align", true)
prop.set("model_scale", Vector3(mul, mul, mul))
prop.set("model_rotation_degrees", Vector3(0.0, yaw, 0.0))
prop.set("can_interact", interact)
## Must set before add_child — ModelProp builds collision in _ready.
prop.set("add_collision", collide)
add_child(prop)
## Editor previews must not be written into village.tscn on save.
prop.owner = null
prop.position = pos
return prop
func _spawn_random_houses() -> void:
var pool := _list_random_house_models()
if pool.is_empty():
push_warning("village: no random house models under %s" % HOUSES_DIR)
return
var i := 0
for slot in HOUSE_SLOTS:
var path: String = pool[randi() % pool.size()]
_spawn_prop(
"RandomHouse_%d" % i,
path,
slot["pos"],
float(slot.get("yaw", 0.0)),
SpriteFit.fit_height_for(path, SpriteFit.WORLD_HOUSE_HEIGHT),
SpriteFit.fit_max_width_for(path, SpriteFit.WORLD_HOUSE_MAX_WIDTH),
float(slot.get("mul", 1.0))
)
i += 1
func _spawn_random_animals() -> void:
var pool := _list_random_animal_models()
if pool.is_empty():
push_warning("village: no random animal models under %s" % ANIMALS_DIR)
return
var i := 0
for slot in ANIMAL_SLOTS:
var path: String = pool[randi() % pool.size()]
var fit_h := SpriteFit.animal_height(path)
var fit_w := SpriteFit.animal_max_width(path)
_spawn_prop(
"RandomAnimal_%d" % i,
path,
slot["pos"],
float(slot.get("yaw", 0.0)),
fit_h,
fit_w,
float(slot.get("mul", 1.0)),
false,
false ## animals do not block walking
)
i += 1
func _spawn_masseur_house() -> void:
if not ResourceLoader.exists(MASSEUR_HOUSE_MODEL):
push_warning("village: massourhaus.glb missing")
return
_spawn_prop(
"MasseurHouse",
MASSEUR_HOUSE_MODEL,
Vector3(-5.5, 0.0, 2.5),
-40.0,
SpriteFit.fit_height_for(MASSEUR_HOUSE_MODEL, SpriteFit.WORLD_HOUSE_HEIGHT),
SpriteFit.fit_max_width_for(MASSEUR_HOUSE_MODEL, SpriteFit.WORLD_HOUSE_MAX_WIDTH),
1.0,
false
)
func _spawn_progress_house() -> void:
## Outdoor: [E] leaves overworld → Catscene (after snake massage).
var pool := _list_random_house_models()
if pool.is_empty():
return
var path: String = pool[0]
var house := _spawn_prop(
"ProgressHouse",
path,
Vector3(0.0, 0.0, -16.0),
0.0,
SpriteFit.fit_height_for(path, SpriteFit.WORLD_HOUSE_HEIGHT),
SpriteFit.fit_max_width_for(path, SpriteFit.WORLD_HOUSE_MAX_WIDTH),
1.0,
true
)
if house == null:
return
house.set("prompt_key", "prompt_enter")
if house.has_signal("interacted"):
house.interacted.connect(_on_progress_house)
func _on_progress_house(_prop: Node = null) -> void:
var ui := get_node_or_null("/root/UIRoot")
if Inventory.has_item("snake") and not GameState.get_flag("snake_massaged", false):
if ui and ui.has_method("show_dialog_key"):
ui.show_dialog_key("dialog_villager_snake")
return
if GameState.get_flag("snake_massaged", false) or GameState.get_flag("bear_saved", false):
GameState.goto_scene("catscene")
return
if not GameState.get_flag("reif_defeated", false):
if ui and ui.has_method("show_dialog_key"):
ui.show_dialog_key("hint_press_button_first", 4.0)
return
if ui and ui.has_method("show_dialog_key"):
ui.show_dialog_key("hint_go_meadow_gate", 4.0)
func _spawn_cow_masseur() -> void:
if not ResourceLoader.exists(COW_MODEL):
push_warning("village: cow.glb missing")
return
var cow := _spawn_prop(
"CowMasseur",
COW_MODEL,
Vector3(-4.2, 0.0, 3.8),
140.0,
SpriteFit.fit_height_for(COW_MODEL, SpriteFit.WORLD_COW_HEIGHT),
SpriteFit.fit_max_width_for(COW_MODEL, 2.4),
1.0,
true,
false ## keep interact range clear
)
if cow == null:
return
cow.set("prompt_key", "prompt_talk")
if cow.has_signal("interacted"):
cow.interacted.connect(_on_cow)
func _on_cow(_npc: Node = null) -> void:
if choice_ui == null:
return
choice_ui.visible = true
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
$ChoiceUI/Panel/Margin/VBox/Label.text = Locale.tr_key("dialog_cow")
$ChoiceUI/Panel/Margin/VBox/MassageBtn.text = Locale.tr_key("choice_massage_now")
$ChoiceUI/Panel/Margin/VBox/DrinkBtn.text = Locale.tr_key("choice_drink_first")
AudioDirector.play_dialog_line("dialog_cow")
func _close_choices() -> void:
if choice_ui:
choice_ui.visible = false
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _on_massage_pressed() -> void:
_close_choices()
if not Inventory.has_item("snake"):
var ui0 := get_node_or_null("/root/UIRoot")
if ui0 and ui0.has_method("show_dialog_key"):
ui0.show_dialog_key("dialog_villager_snake")
return
Inventory.remove_item("snake", 1)
GameState.set_flag("snake_massaged", true)
Achievements.unlock("snake_massage")
if not GameState.get_flag("bear_saved", false):
Inventory.add_item("tooth", 1)
GameState.set_flag("bear_saved", true)
var meadow := get_node_or_null("MeadowZone")
if meadow and meadow.has_method("notify_snake_massaged"):
meadow.notify_snake_massaged()
var ui := get_node_or_null("/root/UIRoot")
if ui and ui.has_method("show_dialog_key"):
ui.show_dialog_key("dialog_cow_massage", 5.0)
func _on_drink_pressed() -> void:
_close_choices()
Inventory.add_item("milk", 1)
var ui := get_node_or_null("/root/UIRoot")
if ui and ui.has_method("show_dialog_key"):
ui.show_dialog_key("dialog_cow_drink", 4.0)
func _setup_flee() -> void:
const FLEE_SKIP := [
"TableButton", "Table", "MasseurHouse", "CowMasseur", "ProgressHouse",
"Exit", "Player", "Ground", "MeadowGround", "WorldEnvironment", "DirectionalLight3D",
"MeadowFence", "MeadowGate", "MeadowZone", "Reif", "TiramisuTonne",
]
for n in get_children():
if n.name in FLEE_SKIP or str(n.name).begins_with(EDITOR_PREVIEW_PREFIX) or str(n.name).begins_with("RandomHouse_") or str(n.name).begins_with("RandomAnimal_") or str(n.name).begins_with("House") or str(n.name).begins_with("Fence"):
continue
if n is ModelProp:
continue
if n.is_in_group("interactable") and n is Node3D:
var tex := str(n.get("texture_path"))
if tex.ends_with("house.png") or tex.ends_with("table.png") or tex.ends_with("button.png"):
continue
_flee_dirs[n] = Vector3(randf_range(-1, 1), 0, randf_range(-1, 1)).normalized()
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return
for n in _flee_dirs.keys():
if not is_instance_valid(n):
continue
var node: Node3D = n
node.global_position += _flee_dirs[n] * 1.8 * delta
if node.global_position.length() > 22.0:
_flee_dirs[n] = -_flee_dirs[n]