extends Control var up_left: Vector2 var down_right: Vector2 var up_right: Vector2 var down_left: Vector2 var elapsed_time: float = 0.0 const STAGES_JSON = "res://data/spawn_stages.json" var stages: Array[SpawnStage] = [] # _state keys: Vector2i(stage_idx, entry_idx) # values: { "timer": float, "alive": int } var _state: Dictionary = {} func _ready() -> void: var camera: Camera2D = get_parent().get_node("Camera2D") var viewport_size = get_viewport_rect().size var world_size = viewport_size / camera.zoom var world_origin = camera.global_position up_left = world_origin down_right = world_origin + world_size up_right = Vector2(down_right.x, up_left.y) down_left = Vector2(up_left.x, down_right.y) _load_stages(STAGES_JSON) for si in stages.size(): for ei in stages[si].entries.size(): _state[Vector2i(si, ei)] = { "timer": 0.0, "alive": 0, "spawned_total": 0 } func _load_stages(path: String) -> void: var file = FileAccess.open(path, FileAccess.READ) if file == null: push_error("spawn_control: cannot open " + path) return var data = JSON.parse_string(file.get_as_text()) if not data is Array: push_error("spawn_control: invalid JSON in " + path) return for sd in data: var stage = SpawnStage.new() stage.time_start = float(sd["time_start"]) stage.time_end = float(sd["time_end"]) for ed in sd["entries"]: var entry = StageEntry.new() entry.enemy = load(ed["enemy"]) entry.count_at_start = int(ed["count_at_start"]) entry.count_at_end = int(ed["count_at_end"]) entry.min_interval = float(ed["min_interval"]) entry.max_spawns = int(ed.get("max_spawns", -1)) stage.entries.append(entry) stages.append(stage) func get_spawn_position() -> Vector2: var side = randi() % 4 var spawn_x: float var spawn_y: float if side == 0: spawn_x = randf_range(up_left.x, up_right.x) spawn_y = up_left.y elif side == 1: spawn_x = up_right.x spawn_y = randf_range(up_right.y, down_right.y) elif side == 2: spawn_x = randf_range(up_left.x, up_right.x) spawn_y = down_left.y else: spawn_x = up_left.x spawn_y = randf_range(up_right.y, down_right.y) return Vector2(spawn_x, spawn_y) func _process(delta: float) -> void: elapsed_time += delta for si in stages.size(): var stage: SpawnStage = stages[si] if elapsed_time < stage.time_start: continue if stage.time_end != -1.0 and elapsed_time > stage.time_end: continue var t: float if stage.time_end == -1.0: t = 1.0 else: t = clamp( (elapsed_time - stage.time_start) / (stage.time_end - stage.time_start), 0.0, 1.0 ) for ei in stage.entries.size(): var entry: StageEntry = stage.entries[ei] var state: Dictionary = _state[Vector2i(si, ei)] if entry.max_spawns != -1 and state["spawned_total"] >= entry.max_spawns: continue var target: int = roundi(lerpf(float(entry.count_at_start), float(entry.count_at_end), t)) var deficit: int = target - state["alive"] if deficit <= 0: continue state["timer"] -= delta if state["timer"] <= 0.0: state["timer"] = max(entry.min_interval, 1.0 / float(deficit)) _spawn_one(entry, state) func _spawn_one(entry: StageEntry, state: Dictionary) -> void: var enemy = entry.enemy.instantiate() enemy.global_position = get_spawn_position() state["alive"] += 1 state["spawned_total"] += 1 enemy.tree_exited.connect(func(): state["alive"] -= 1) add_child(enemy)