extends Node3D var boxes: Array[CSGBox3D] var points: Array[Vector3] const MAX_CACHED_POINTS := 100 var doomed_point_idx := 0 @export var enemy_scenes: Array[EnemyEntry] var weights: Array[float] var total_cost := 0. var money := 5. var rng := RandomNumberGenerator.new() func _get_point() -> Vector3: assert(not boxes.is_empty()) var box: CSGBox3D = boxes.pick_random() var x_stride := box.size.x / 2 var y_stride := box.size.y / 2 var ret := box.global_position ret.x += randf_range(-x_stride, x_stride) ret.y += randf_range(-y_stride, y_stride) return ret func _ready() -> void: for child in get_children(): if child is CSGBox3D: boxes.push_back(child) for entry in enemy_scenes: total_cost += entry.cost for entry in enemy_scenes: weights.push_back(total_cost - entry.cost + 1.) func _process(_delta: float) -> void: var v := _get_point() v.y = 50. var col_mask := 16 # floor collision layer var params := PhysicsRayQueryParameters3D.create( v, v + Vector3.DOWN * 100., 1 # detect any level geometry ) var raycast := get_world_3d().direct_space_state.intersect_ray(params) # if the raycast hit and the hit was the floor... if not raycast.is_empty() and raycast.collider.collision_layer & col_mask: var hitpos: Vector3 = raycast.position if points.size() < MAX_CACHED_POINTS: points.push_back(hitpos) else: points[doomed_point_idx] = hitpos doomed_point_idx += 1 doomed_point_idx = doomed_point_idx % MAX_CACHED_POINTS func _on_timer_timeout() -> void: if not Level.is_active(): return money += 2. if randf() > .1: return if points.is_empty(): push_warning("tried to spawn enemy, but can't find a point!") return while money > 3.: var entry: EnemyEntry = enemy_scenes.pick_random() if entry.cost > money: return var scene := entry.scene var pos: Vector3 = points.pick_random() pos += Vector3.UP * 0.02 var car: Node3D = scene.instantiate() car.position = pos get_tree().current_scene.add_child(car) money -= entry.cost