implement cluster enemy
This commit is contained in:
parent
11be4f21ac
commit
d2ecfcafe9
@ -93,6 +93,9 @@ const Enemy = struct {
|
||||
target: ?Vec2 = null,
|
||||
distance_to_target: ?f32 = null,
|
||||
|
||||
avoidance_group: ?[]Enemy.Id = null,
|
||||
avoidance_group_distance: ?f32 = null,
|
||||
|
||||
dead: bool = false,
|
||||
|
||||
const List = GenerationalArrayList(Enemy);
|
||||
@ -101,10 +104,12 @@ const Enemy = struct {
|
||||
|
||||
pub const Manager = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
kind: union(enum) {
|
||||
snake: struct {
|
||||
enemies: std.ArrayList(Enemy.Id) = .empty,
|
||||
},
|
||||
center_enemy: ?Enemy.Id = null,
|
||||
center_enemy_pos: ?Vec2 = null,
|
||||
kind: enum {
|
||||
snake,
|
||||
cluster
|
||||
},
|
||||
|
||||
pub fn deinit(self: Manager) void {
|
||||
@ -122,12 +127,13 @@ const Wave = struct {
|
||||
enemies_spawned: u32,
|
||||
total_enemies: u32,
|
||||
|
||||
min_snake_length: u32,
|
||||
max_snake_length: u32,
|
||||
min_group_size: u32,
|
||||
max_group_size: u32,
|
||||
|
||||
const Kind = enum {
|
||||
regular,
|
||||
snake
|
||||
snake,
|
||||
cluster
|
||||
};
|
||||
|
||||
const Info = struct {
|
||||
@ -136,8 +142,8 @@ const Wave = struct {
|
||||
duration_s: u32,
|
||||
starts_at_s: u32,
|
||||
|
||||
min_snake_length: u32 = 3,
|
||||
max_snake_length: u32 = 5,
|
||||
min_group_size: u32 = 3,
|
||||
max_group_size: u32 = 5,
|
||||
};
|
||||
};
|
||||
|
||||
@ -154,19 +160,27 @@ const world_size = Vec2.init(20 * 16, 15 * 16);
|
||||
const invincibility_duration_s = 0.5;
|
||||
const pickup_spawn_duration_s = Range.init(1, 5);
|
||||
const wave_infos = [_]Wave.Info{
|
||||
// .{
|
||||
// .kind = .regular,
|
||||
// .enemies = 10,
|
||||
// .duration_s = 10,
|
||||
// .starts_at_s = 0
|
||||
// },
|
||||
.{
|
||||
.kind = .regular,
|
||||
.enemies = 10,
|
||||
.duration_s = 10,
|
||||
.starts_at_s = 0
|
||||
},
|
||||
Wave.Info{
|
||||
.kind = .snake,
|
||||
.enemies = 1,
|
||||
.duration_s = 1,
|
||||
.starts_at_s = 0,
|
||||
.min_snake_length = 5,
|
||||
.max_snake_length = 5
|
||||
.min_group_size = 5,
|
||||
.max_group_size = 5
|
||||
},
|
||||
Wave.Info{
|
||||
.kind = .cluster,
|
||||
.enemies = 1,
|
||||
.duration_s = 1,
|
||||
.starts_at_s = 0,
|
||||
.min_group_size = 30,
|
||||
.max_group_size = 30
|
||||
}
|
||||
};
|
||||
|
||||
@ -229,37 +243,30 @@ pub fn deinit(self: *CombatScreen) void {
|
||||
}
|
||||
|
||||
const EnemyOptions = struct {
|
||||
pos: ?Vec2 = null
|
||||
pos: ?Vec2 = null,
|
||||
size: f32 = 20
|
||||
};
|
||||
|
||||
pub fn spawnEnemy(self: *CombatScreen, opts: EnemyOptions) !Enemy.Id {
|
||||
|
||||
var pos: Vec2 = undefined;
|
||||
if (opts.pos) |opts_pos| {
|
||||
pos = opts_pos;
|
||||
} else {
|
||||
const spawn_area_margin = 20;
|
||||
const spawn_area_size = 10;
|
||||
|
||||
fn pickSpawnLocation(rand: std.Random, margin: f32, size: f32) Vec2 {
|
||||
const top_spawn_area = (Rect{
|
||||
.pos = .init(0, -spawn_area_size - spawn_area_margin),
|
||||
.size = .init(world_size.x, spawn_area_size)
|
||||
}).growX(spawn_area_size);
|
||||
.pos = .init(0, -size - margin),
|
||||
.size = .init(world_size.x, size)
|
||||
}).growX(size);
|
||||
|
||||
const bottom_spawn_area = (Rect{
|
||||
.pos = .init(0, world_size.y + spawn_area_margin),
|
||||
.size = .init(world_size.x, spawn_area_size)
|
||||
}).growX(spawn_area_size);
|
||||
.pos = .init(0, world_size.y + margin),
|
||||
.size = .init(world_size.x, size)
|
||||
}).growX(size);
|
||||
|
||||
const left_spawn_area = (Rect{
|
||||
.pos = .init(-spawn_area_margin-spawn_area_size, 0),
|
||||
.size = .init(spawn_area_size, world_size.y)
|
||||
}).growY(spawn_area_size);
|
||||
.pos = .init(-margin-size, 0),
|
||||
.size = .init(size, world_size.y)
|
||||
}).growY(size);
|
||||
|
||||
const right_spawn_area = (Rect{
|
||||
.pos = .init(world_size.x + spawn_area_margin, 0),
|
||||
.size = .init(spawn_area_size, world_size.y)
|
||||
}).growY(spawn_area_size);
|
||||
.pos = .init(world_size.x + margin, 0),
|
||||
.size = .init(size, world_size.y)
|
||||
}).growY(size);
|
||||
|
||||
const spawn_areas = [_]Rect{
|
||||
top_spawn_area,
|
||||
@ -268,15 +275,22 @@ pub fn spawnEnemy(self: *CombatScreen, opts: EnemyOptions) !Enemy.Id {
|
||||
right_spawn_area
|
||||
};
|
||||
|
||||
const rand = self.rng.random();
|
||||
const spawn_area = spawn_areas[rand.uintLessThan(usize, spawn_areas.len)];
|
||||
pos = Vec2.initRandomRect(rand, spawn_area);
|
||||
return Vec2.initRandomRect(rand, spawn_area);
|
||||
}
|
||||
|
||||
pub fn spawnEnemy(self: *CombatScreen, opts: EnemyOptions) !Enemy.Id {
|
||||
var pos: Vec2 = undefined;
|
||||
if (opts.pos) |opts_pos| {
|
||||
pos = opts_pos;
|
||||
} else {
|
||||
pos = pickSpawnLocation(self.rng.random(), 20, 10);
|
||||
}
|
||||
|
||||
return try self.enemies.insert(self.gpa, Enemy{
|
||||
.kinetic = .{ .pos = pos },
|
||||
.speed = 50,
|
||||
.size = 20
|
||||
.size = opts.size
|
||||
});
|
||||
}
|
||||
|
||||
@ -323,8 +337,8 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
.duration = @as(u64, wave_info.duration_s) * std.time.ns_per_s,
|
||||
.enemies_spawned = 0,
|
||||
.total_enemies = wave_info.enemies,
|
||||
.min_snake_length = wave_info.min_snake_length,
|
||||
.max_snake_length = wave_info.max_snake_length,
|
||||
.min_group_size = wave_info.min_group_size,
|
||||
.max_group_size = wave_info.max_group_size,
|
||||
});
|
||||
}
|
||||
|
||||
@ -349,9 +363,9 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
var arena = std.heap.ArenaAllocator.init(self.gpa);
|
||||
var enemies: std.ArrayList(Enemy.Id) =.empty;
|
||||
|
||||
assert(wave.min_snake_length >= 1);
|
||||
assert(wave.min_group_size >= 1);
|
||||
const rand = self.rng.random();
|
||||
const snake_length = wave.min_snake_length + rand.uintAtMost(u32, wave.max_snake_length - wave.min_snake_length);
|
||||
const snake_length = rand.intRangeAtMost(u32, wave.min_group_size, wave.max_group_size);
|
||||
|
||||
const head_id = try self.spawnEnemy(.{ });
|
||||
try enemies.append(arena.allocator(), head_id);
|
||||
@ -372,11 +386,36 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
|
||||
_ = try self.managers.insert(self.gpa, Manager{
|
||||
.arena = arena,
|
||||
.kind = .{
|
||||
.snake = .{
|
||||
.enemies = enemies
|
||||
}
|
||||
.enemies = enemies,
|
||||
.kind = .snake
|
||||
});
|
||||
},
|
||||
.cluster => {
|
||||
var arena = std.heap.ArenaAllocator.init(self.gpa);
|
||||
var enemies: std.ArrayList(Enemy.Id) =.empty;
|
||||
|
||||
const min_cluster_radius = 10;
|
||||
const max_cluster_radius = 20;
|
||||
|
||||
const rand = self.rng.random();
|
||||
const center = pickSpawnLocation(rand, 20 + max_cluster_radius, 10);
|
||||
|
||||
const group_size = rand.intRangeAtMost(u32, wave.min_group_size, wave.max_group_size);
|
||||
for (0..group_size) |_| {
|
||||
const angle = rand.float(f32) * std.math.pi * 2;
|
||||
const distance = min_cluster_radius + rand.float(f32) * (max_cluster_radius - min_cluster_radius);
|
||||
|
||||
const enemy_id = try self.spawnEnemy(.{
|
||||
.pos = center.add(Vec2.initAngle(angle).multiplyScalar(distance)),
|
||||
.size = 10
|
||||
});
|
||||
try enemies.append(arena.allocator(), enemy_id);
|
||||
}
|
||||
|
||||
_ = try self.managers.insert(self.gpa, Manager{
|
||||
.arena = arena,
|
||||
.enemies = enemies,
|
||||
.kind = .cluster
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -604,47 +643,80 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
var enemy_iter = self.enemies.iterator();
|
||||
while (enemy_iter.nextItem()) |enemy| {
|
||||
enemy.target = null;
|
||||
enemy.distance_to_target = null;
|
||||
enemy.avoidance_group = null;
|
||||
enemy.avoidance_group_distance = null;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var manager_iter = self.managers.iterator();
|
||||
while (manager_iter.next()) |tuple| {
|
||||
var destroy: bool = false;
|
||||
const manager_id = tuple.id;
|
||||
const manager = tuple.item;
|
||||
|
||||
switch (manager.kind) {
|
||||
.snake => |*snake| {
|
||||
var managed: std.ArrayList(*Enemy) = .empty;
|
||||
|
||||
var index: usize = 0;
|
||||
while (index < snake.enemies.items.len) {
|
||||
const enemy_id = snake.enemies.items[index];
|
||||
if (self.enemies.exists(enemy_id)) {
|
||||
while (index < manager.enemies.items.len) {
|
||||
const enemy_id = manager.enemies.items[index];
|
||||
if (self.enemies.get(enemy_id)) |enemy| {
|
||||
try managed.append(frame.arena.allocator(), enemy);
|
||||
index += 1;
|
||||
} else {
|
||||
_ = snake.enemies.orderedRemove(index);
|
||||
_ = manager.enemies.orderedRemove(index);
|
||||
}
|
||||
}
|
||||
|
||||
for (1..snake.enemies.items.len) |i| {
|
||||
const enemy_id = snake.enemies.items[i];
|
||||
const enemy = self.enemies.getAssumeExists(enemy_id);
|
||||
const follow_enemy_id = snake.enemies.items[i-1];
|
||||
const follow_enemy = self.enemies.getAssumeExists(follow_enemy_id);
|
||||
if (manager.enemies.items.len <= 1) {
|
||||
manager.deinit();
|
||||
self.managers.removeAssumeExists(manager_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (manager.kind) {
|
||||
.snake => {
|
||||
for (1..managed.items.len) |i| {
|
||||
const enemy = managed.items[i];
|
||||
const follow_enemy = managed.items[i-1];
|
||||
|
||||
enemy.target = follow_enemy.kinetic.pos;
|
||||
enemy.distance_to_target = 20;
|
||||
}
|
||||
|
||||
if (snake.enemies.items.len <= 1) {
|
||||
destroy = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
.cluster => {
|
||||
if (manager.center_enemy != null and !self.enemies.exists(manager.center_enemy.?)) {
|
||||
manager.center_enemy = null;
|
||||
}
|
||||
|
||||
if (destroy) {
|
||||
manager.deinit();
|
||||
self.managers.removeAssumeExists(manager_id);
|
||||
if (manager.center_enemy == null) {
|
||||
if (manager.center_enemy_pos == null) {
|
||||
manager.center_enemy = manager.enemies.items[0];
|
||||
} else {
|
||||
var closest_index: usize = 0;
|
||||
for (1..managed.items.len) |enemy_index| {
|
||||
const dist = managed.items[enemy_index].kinetic.pos.distanceSqr(manager.center_enemy_pos.?);
|
||||
const closest_dist = managed.items[closest_index].kinetic.pos.distanceSqr(manager.center_enemy_pos.?);
|
||||
if (closest_dist > dist) {
|
||||
closest_index = enemy_index;
|
||||
}
|
||||
}
|
||||
manager.center_enemy = manager.enemies.items[closest_index];
|
||||
}
|
||||
}
|
||||
const center_enemy_id = manager.center_enemy.?;
|
||||
const center_enemy = self.enemies.getAssumeExists(center_enemy_id);
|
||||
manager.center_enemy_pos = center_enemy.kinetic.pos;
|
||||
|
||||
for (0.., managed.items) |i, enemy| {
|
||||
const enemy_id = manager.enemies.items[i];
|
||||
if (enemy_id != center_enemy_id) {
|
||||
enemy.target = center_enemy.kinetic.pos;
|
||||
enemy.avoidance_group = try frame.arena.allocator().dupe(Enemy.Id, manager.enemies.items);
|
||||
enemy.avoidance_group_distance = 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -656,19 +728,36 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
|
||||
enemy.target = self.player.kinetic.pos;
|
||||
}
|
||||
|
||||
var apply_vel = true;
|
||||
const to_target = enemy.target.?.sub(enemy.kinetic.pos);
|
||||
var target = enemy.target.?;
|
||||
if (enemy.distance_to_target) |distance_to_target| {
|
||||
apply_vel = to_target.length() > distance_to_target;
|
||||
const to_target = enemy.target.?.sub(enemy.kinetic.pos);
|
||||
const dir_to_target = to_target.normalized();
|
||||
target = target.sub(dir_to_target.multiplyScalar(distance_to_target));
|
||||
}
|
||||
|
||||
if (apply_vel) {
|
||||
var avoidance: Vec2 = .zero;
|
||||
if (enemy.avoidance_group) |avoidance_group| {
|
||||
const avoidance_distance = enemy.avoidance_group_distance.?;
|
||||
for (avoidance_group) |other_enemy_id| {
|
||||
const other_enemy = self.enemies.get(other_enemy_id) orelse continue;
|
||||
const dir_away_from_other = other_enemy.kinetic.pos.sub(enemy.kinetic.pos);
|
||||
const dist_to_other = dir_away_from_other.length();
|
||||
if (dist_to_other < avoidance_distance) {
|
||||
avoidance = avoidance.sub(dir_away_from_other.normalized().multiplyScalar(avoidance_distance - dist_to_other));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const to_target = target.sub(enemy.kinetic.pos);
|
||||
if (to_target.length() > 1) {
|
||||
const dir_to_target = to_target.normalized();
|
||||
enemy.kinetic.vel = dir_to_target.multiplyScalar(enemy.speed);
|
||||
} else {
|
||||
enemy.kinetic.vel = .zero;
|
||||
}
|
||||
|
||||
enemy.kinetic.vel = enemy.kinetic.vel.add(avoidance.multiplyScalar(5));
|
||||
|
||||
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
||||
if (enemy_rect.hasOverlap(self.player.getRect())) {
|
||||
var is_invincible = false;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user