Wave escalation and the Snippest boss
Escalation (EnemySpawner exports, all tunable): - Wave size grows +1 enemy per wave, capped at 20 - Enemy health +15% and damage +10% per wave, applied identically on every peer via the spawn RPC before _ready (so hitbox stats and current_health pick them up) Boss milestone every 5th wave: the Snippest - the basic lobster at 5x scale and 5x combat stats (50 HP, 25 damage, 5x knockback and reach). Movement deliberately not 5x (1.2x - it lumbers). Pays 250 gold and always drops a health orb. Wave escalation multipliers stack on top, so later bosses keep scaling. Boss scene instances basic_enemy.tscn with the BossLobster script; visual/collision children are scaled uniformly in code (not the CharacterBody3D root, not shared shape resources).
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
extends BasicEnemy
|
||||
class_name BossLobster
|
||||
## The Snippest itself: a lobster five times the size with five times the stats.
|
||||
## Spawned by the EnemySpawner on boss milestone waves.
|
||||
|
||||
const BOSS_MULTIPLIER := 5.0
|
||||
|
||||
func _ready():
|
||||
# 5x combat stats - applied BEFORE super._ready() so the hitbox
|
||||
# picks them up via set_stats(attack_damage, attack_knockback)
|
||||
max_health *= BOSS_MULTIPLIER
|
||||
attack_damage *= BOSS_MULTIPLIER
|
||||
attack_knockback *= BOSS_MULTIPLIER
|
||||
attack_range *= BOSS_MULTIPLIER # A 5x lobster has 5x reach
|
||||
|
||||
# Deliberately NOT 5x: movement (17.5 speed would be inescapable).
|
||||
# Big things lumber. Tune here if the boss should be faster.
|
||||
move_speed *= 1.2
|
||||
|
||||
# Boss rewards: jackpot gold, guaranteed health orb
|
||||
gold_reward = 250
|
||||
health_orb_drop_chance = 1.0
|
||||
|
||||
super._ready()
|
||||
|
||||
_apply_boss_scale()
|
||||
|
||||
## Uniformly scale the visual and collision children 5x around the root.
|
||||
## Child-node scaling (not root CharacterBody3D scale, not shared shape
|
||||
## resources) keeps physics sane and other lobsters unaffected.
|
||||
func _apply_boss_scale():
|
||||
for child_name in ["Mesh", "CollisionShape3D", "HurtBox", "HitBox"]:
|
||||
var child = get_node_or_null(child_name)
|
||||
if child:
|
||||
child.transform = child.transform.scaled(Vector3.ONE * BOSS_MULTIPLIER)
|
||||
@@ -25,6 +25,14 @@ signal all_enemies_defeated()
|
||||
@export_category("Enemy Pool")
|
||||
@export var enemy_scenes: Array[PackedScene] = [] ## List of enemy scenes to spawn from
|
||||
|
||||
@export_category("Escalation")
|
||||
@export var wave_count_growth: float = 1.0 ## Extra enemies added per wave
|
||||
@export var max_enemies_per_wave: int = 20 ## Hard cap on wave size
|
||||
@export var health_growth_per_wave: float = 0.15 ## +15% enemy health per wave
|
||||
@export var damage_growth_per_wave: float = 0.10 ## +10% enemy damage per wave
|
||||
@export var boss_wave_interval: int = 5 ## Every Nth wave is a boss wave (0 = never)
|
||||
@export var boss_scene: PackedScene = null ## The Snippest itself
|
||||
|
||||
## Wave tracking
|
||||
var current_wave: int = 0
|
||||
var active_enemies: Array[Node] = []
|
||||
@@ -74,9 +82,46 @@ func start_wave():
|
||||
# Broadcast wave number to every peer's GameState (for HUD + run stats)
|
||||
GameState.server_sync_wave(current_wave)
|
||||
|
||||
# Spawn enemies
|
||||
for i in range(enemies_per_wave):
|
||||
_spawn_enemy(i, enemies_per_wave)
|
||||
# Boss milestone waves field the Snippest instead of a normal wave
|
||||
if _is_boss_wave(current_wave):
|
||||
print("[EnemySpawner] BOSS WAVE ", current_wave)
|
||||
_spawn_boss()
|
||||
else:
|
||||
var count = _wave_enemy_count(current_wave)
|
||||
for i in range(count):
|
||||
_spawn_enemy(i, count)
|
||||
|
||||
## Every Nth wave, if a boss scene is configured
|
||||
func _is_boss_wave(wave: int) -> bool:
|
||||
return boss_wave_interval > 0 and boss_scene != null and wave % boss_wave_interval == 0
|
||||
|
||||
## Wave size grows linearly, capped
|
||||
func _wave_enemy_count(wave: int) -> int:
|
||||
return mini(enemies_per_wave + int((wave - 1) * wave_count_growth), max_enemies_per_wave)
|
||||
|
||||
func _wave_health_mult(wave: int) -> float:
|
||||
return 1.0 + (wave - 1) * health_growth_per_wave
|
||||
|
||||
func _wave_damage_mult(wave: int) -> float:
|
||||
return 1.0 + (wave - 1) * damage_growth_per_wave
|
||||
|
||||
## Spawn the boss at a random point on the spawn circle (server only)
|
||||
func _spawn_boss():
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
_enemy_id_counter += 1
|
||||
var boss_name = "Boss_" + str(current_wave) + "_" + str(_enemy_id_counter)
|
||||
|
||||
var angle = randf() * TAU
|
||||
var spawn_pos = global_position + Vector3(
|
||||
cos(angle) * spawn_radius,
|
||||
spawn_height,
|
||||
sin(angle) * spawn_radius
|
||||
)
|
||||
|
||||
rpc("_spawn_enemy_on_client", boss_name, spawn_pos, boss_scene.resource_path,
|
||||
_wave_health_mult(current_wave), _wave_damage_mult(current_wave))
|
||||
|
||||
## Spawn a single enemy at a position around the circle
|
||||
func _spawn_enemy(index: int, total: int):
|
||||
@@ -108,11 +153,13 @@ func _spawn_enemy(index: int, total: int):
|
||||
|
||||
# Spawn on all clients via RPC (call_local will spawn on server too)
|
||||
var enemy_scene_path = enemy_scene.resource_path
|
||||
rpc("_spawn_enemy_on_client", enemy_name, spawn_pos, enemy_scene_path)
|
||||
rpc("_spawn_enemy_on_client", enemy_name, spawn_pos, enemy_scene_path,
|
||||
_wave_health_mult(current_wave), _wave_damage_mult(current_wave))
|
||||
|
||||
## RPC to spawn enemy on all clients (including server via call_local)
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _spawn_enemy_on_client(enemy_name: String, spawn_pos: Vector3, scene_path: String):
|
||||
func _spawn_enemy_on_client(enemy_name: String, spawn_pos: Vector3, scene_path: String,
|
||||
health_mult: float = 1.0, damage_mult: float = 1.0):
|
||||
# Load the enemy scene
|
||||
var enemy_scene = load(scene_path)
|
||||
if not enemy_scene:
|
||||
@@ -124,6 +171,14 @@ func _spawn_enemy_on_client(enemy_name: String, spawn_pos: Vector3, scene_path:
|
||||
enemy.name = enemy_name
|
||||
enemy.position = spawn_pos
|
||||
|
||||
# Wave escalation - set BEFORE add_child so _ready picks the values up
|
||||
# (current_health = max_health in BaseUnit, hitbox set_stats in BasicEnemy).
|
||||
# Applied identically on every peer; the boss's own 5x stacks on top.
|
||||
if health_mult != 1.0 and "max_health" in enemy:
|
||||
enemy.max_health *= health_mult
|
||||
if damage_mult != 1.0 and "attack_damage" in enemy:
|
||||
enemy.attack_damage *= damage_mult
|
||||
|
||||
# Find enemies container
|
||||
var level = get_tree().get_current_scene()
|
||||
var enemies_container = null
|
||||
|
||||
Reference in New Issue
Block a user