move combat screen to separate file
This commit is contained in:
parent
9d62e0eb2f
commit
365c7a0163
444
src/combat_screen.zig
Normal file
444
src/combat_screen.zig
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const Assets = @import("./assets.zig");
|
||||||
|
|
||||||
|
const Engine = @import("./engine/root.zig");
|
||||||
|
const Nanoseconds = Engine.Nanoseconds;
|
||||||
|
const Vec2 = Engine.Vec2;
|
||||||
|
const Rect = Engine.Math.Rect;
|
||||||
|
const Range = Engine.Math.Range;
|
||||||
|
const rgb = Engine.Math.rgb;
|
||||||
|
const rgba = Engine.Math.rgba;
|
||||||
|
|
||||||
|
const CombatScreen = @This();
|
||||||
|
|
||||||
|
const RNGState = std.Random.DefaultPrng;
|
||||||
|
|
||||||
|
const Kinetic = struct {
|
||||||
|
pos: Vec2 = .zero,
|
||||||
|
vel: Vec2 = .zero,
|
||||||
|
acc: Vec2 = .zero,
|
||||||
|
|
||||||
|
const UpdateOptions = struct {
|
||||||
|
max_speed: ?f32 = null,
|
||||||
|
friction: ?f32 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn update(self: *Kinetic, dt: f32, opts: UpdateOptions) void {
|
||||||
|
self.vel = self.vel.add(self.acc.multiplyScalar(dt));
|
||||||
|
if (opts.max_speed) |max_speed| {
|
||||||
|
self.vel = self.vel.limitLength(max_speed);
|
||||||
|
}
|
||||||
|
if (opts.friction) |friction| {
|
||||||
|
const friction_force = std.math.pow(f32, 1 - friction, dt);
|
||||||
|
self.vel = self.vel.multiplyScalar(friction_force);
|
||||||
|
}
|
||||||
|
self.pos = self.pos.add(self.vel.multiplyScalar(dt));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Player = struct {
|
||||||
|
kinetic: Kinetic = .{},
|
||||||
|
money: u32 = 0,
|
||||||
|
health: u32 = 0,
|
||||||
|
max_health: u32 = 0,
|
||||||
|
last_shot_at: ?Nanoseconds = null,
|
||||||
|
invincible_until: ?Nanoseconds = null,
|
||||||
|
|
||||||
|
pub fn getRect(self: Player) Rect {
|
||||||
|
return getCenteredRect(self.kinetic.pos, 16);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Bullet = struct {
|
||||||
|
kinetic: Kinetic,
|
||||||
|
size: f32 = 5,
|
||||||
|
dir: Vec2,
|
||||||
|
speed: f32,
|
||||||
|
|
||||||
|
dead: bool = false
|
||||||
|
};
|
||||||
|
|
||||||
|
const Enemy = struct {
|
||||||
|
kinetic: Kinetic,
|
||||||
|
speed: f32,
|
||||||
|
size: f32,
|
||||||
|
|
||||||
|
dead: bool = false
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wave = struct {
|
||||||
|
started_at: Nanoseconds,
|
||||||
|
duration: Nanoseconds,
|
||||||
|
enemies_spawned: u32,
|
||||||
|
total_enemies: u32,
|
||||||
|
|
||||||
|
const Info = struct {
|
||||||
|
enemies: u32,
|
||||||
|
duration_s: u32,
|
||||||
|
starts_at_s: u32,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Pickup = struct {
|
||||||
|
const Kind = enum {
|
||||||
|
money
|
||||||
|
};
|
||||||
|
|
||||||
|
pos: Vec2,
|
||||||
|
kind: Kind,
|
||||||
|
};
|
||||||
|
|
||||||
|
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{
|
||||||
|
.{
|
||||||
|
.enemies = 10,
|
||||||
|
.duration_s = 10,
|
||||||
|
.starts_at_s = 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
gpa: Allocator,
|
||||||
|
assets: *Assets,
|
||||||
|
rng: RNGState,
|
||||||
|
|
||||||
|
player: Player = .{},
|
||||||
|
bullets: std.ArrayList(Bullet) = .empty,
|
||||||
|
enemies: std.ArrayList(Enemy) = .empty,
|
||||||
|
|
||||||
|
pickups: std.ArrayList(Pickup) = .empty,
|
||||||
|
next_pickup_spawn_at: Nanoseconds,
|
||||||
|
|
||||||
|
wave_timer: Nanoseconds = 0,
|
||||||
|
|
||||||
|
waves: std.ArrayList(Wave) = .empty,
|
||||||
|
spawned_waves: std.ArrayList(usize) = .empty,
|
||||||
|
|
||||||
|
pub fn init(gpa: Allocator, seed: u64, assets: *Assets) CombatScreen {
|
||||||
|
var rng = RNGState.init(seed);
|
||||||
|
|
||||||
|
const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(rng.random());
|
||||||
|
const next_pickup_spawn_at: Nanoseconds = @intFromFloat(next_pickup_spawn_at_s * std.time.ns_per_s);
|
||||||
|
|
||||||
|
return CombatScreen{
|
||||||
|
.gpa = gpa,
|
||||||
|
.assets = assets,
|
||||||
|
.next_pickup_spawn_at = next_pickup_spawn_at,
|
||||||
|
.player = .{
|
||||||
|
.kinetic = .{
|
||||||
|
.pos = .init(50, 50),
|
||||||
|
},
|
||||||
|
.health = 3,
|
||||||
|
.max_health = 3
|
||||||
|
},
|
||||||
|
.rng = rng
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *CombatScreen) void {
|
||||||
|
self.bullets.deinit(self.gpa);
|
||||||
|
self.enemies.deinit(self.gpa);
|
||||||
|
self.waves.deinit(self.gpa);
|
||||||
|
self.pickups.deinit(self.gpa);
|
||||||
|
self.spawned_waves.deinit(self.gpa);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawnEnemy(self: *CombatScreen) !void {
|
||||||
|
const spawn_area_margin = 20;
|
||||||
|
const spawn_area_size = 10;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const spawn_areas = [_]Rect{
|
||||||
|
top_spawn_area,
|
||||||
|
bottom_spawn_area,
|
||||||
|
left_spawn_area,
|
||||||
|
right_spawn_area
|
||||||
|
};
|
||||||
|
|
||||||
|
const rand = self.rng.random();
|
||||||
|
const spawn_area = spawn_areas[rand.uintLessThan(usize, spawn_areas.len)];
|
||||||
|
const pos = Vec2.initRandomRect(rand, spawn_area);
|
||||||
|
|
||||||
|
try self.enemies.append(self.gpa, .{
|
||||||
|
.kinetic = .{ .pos = pos },
|
||||||
|
.speed = 10,
|
||||||
|
.size = 20
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawnPickup(self: *CombatScreen) !void {
|
||||||
|
const margin = 10;
|
||||||
|
const spawn_area = (Rect{ .pos = .zero, .size = world_size }).grow(-margin);
|
||||||
|
const pos = Vec2.initRandomRect(self.rng.random(), spawn_area);
|
||||||
|
|
||||||
|
try self.pickups.append(self.gpa, Pickup{
|
||||||
|
.pos = pos,
|
||||||
|
.kind = .money
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(self: *CombatScreen, frame: *Engine.Frame) !void {
|
||||||
|
const dt = frame.deltaTime();
|
||||||
|
|
||||||
|
self.wave_timer += frame.dt_ns;
|
||||||
|
const wave_timer_s = @divFloor(self.wave_timer, std.time.ns_per_s);
|
||||||
|
|
||||||
|
for (0.., wave_infos) |i, wave_info| {
|
||||||
|
if (std.mem.indexOfScalar(usize, self.spawned_waves.items, i) != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wave_info.starts_at_s > wave_timer_s) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.spawned_waves.append(self.gpa, i);
|
||||||
|
try self.waves.append(self.gpa, .{
|
||||||
|
.started_at = self.wave_timer,
|
||||||
|
.duration = @as(u64, wave_info.duration_s) * std.time.ns_per_s,
|
||||||
|
.enemies_spawned = 0,
|
||||||
|
.total_enemies = wave_info.enemies
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var index: usize = 0;
|
||||||
|
while (index < self.waves.items.len) {
|
||||||
|
var wave_complete = false;
|
||||||
|
const wave = &self.waves.items[index];
|
||||||
|
|
||||||
|
const wave_time_passed: f32 = @floatFromInt(self.wave_timer - wave.started_at);
|
||||||
|
const wave_duration: f32 = @floatFromInt(wave.duration);
|
||||||
|
const percent_complete = std.math.clamp(wave_time_passed / wave_duration, 0, 1);
|
||||||
|
const wave_total_enemies: f32 = @floatFromInt(wave.total_enemies);
|
||||||
|
const expected_enemies: u32 = @intFromFloat(wave_total_enemies * percent_complete);
|
||||||
|
|
||||||
|
while (wave.enemies_spawned < expected_enemies) {
|
||||||
|
try self.spawnEnemy();
|
||||||
|
wave.enemies_spawned += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wave.enemies_spawned == wave.total_enemies) {
|
||||||
|
wave_complete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wave_complete) {
|
||||||
|
_ = self.waves.swapRemove(index);
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.wave_timer >= self.next_pickup_spawn_at) {
|
||||||
|
const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(self.rng.random());
|
||||||
|
const next_pickup_spawn_at: Nanoseconds = @intFromFloat(next_pickup_spawn_at_s * std.time.ns_per_s);
|
||||||
|
self.next_pickup_spawn_at = self.wave_timer + next_pickup_spawn_at;
|
||||||
|
try self.spawnPickup();
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.graphics.canvas_size = world_size;
|
||||||
|
|
||||||
|
frame.drawRectangle(.{
|
||||||
|
.rect = .init(0, 0, world_size.x, world_size.y),
|
||||||
|
.color = rgb(20, 20, 20)
|
||||||
|
});
|
||||||
|
|
||||||
|
var dir = Vec2.init(0, 0);
|
||||||
|
if (frame.isKeyDown(.W)) {
|
||||||
|
dir.y -= 1;
|
||||||
|
}
|
||||||
|
if (frame.isKeyDown(.S)) {
|
||||||
|
dir.y += 1;
|
||||||
|
}
|
||||||
|
if (frame.isKeyDown(.A)) {
|
||||||
|
dir.x -= 1;
|
||||||
|
}
|
||||||
|
if (frame.isKeyDown(.D)) {
|
||||||
|
dir.x += 1;
|
||||||
|
}
|
||||||
|
dir = dir.normalized();
|
||||||
|
|
||||||
|
const acceleration = 1500;
|
||||||
|
|
||||||
|
self.player.kinetic.acc = dir.multiplyScalar(acceleration);
|
||||||
|
self.player.kinetic.update(dt, .{
|
||||||
|
.friction = 0.99988,
|
||||||
|
.max_speed = 400
|
||||||
|
});
|
||||||
|
|
||||||
|
if (frame.input.mouse_position) |mouse| {
|
||||||
|
const cursor_size = Vec2.init(10, 10);
|
||||||
|
frame.drawRectangle(.{
|
||||||
|
.rect = .{
|
||||||
|
.pos = mouse.sub(cursor_size.divideScalar(2)),
|
||||||
|
.size = cursor_size,
|
||||||
|
},
|
||||||
|
.color = rgba(255, 200, 200, 0.5)
|
||||||
|
});
|
||||||
|
|
||||||
|
const bullet_dir = mouse.sub(self.player.kinetic.pos).normalized();
|
||||||
|
|
||||||
|
var cooldown_complete = true;
|
||||||
|
if (self.player.last_shot_at) |last_shot_at| {
|
||||||
|
const cooldown_ns = std.time.ns_per_ms * 500;
|
||||||
|
cooldown_complete = frame.time_ns > last_shot_at + cooldown_ns;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.isMouseDown(.left) and cooldown_complete) {
|
||||||
|
self.player.last_shot_at = frame.time_ns;
|
||||||
|
try self.bullets.append(self.gpa, .{
|
||||||
|
.kinetic = .{
|
||||||
|
.pos = self.player.kinetic.pos,
|
||||||
|
},
|
||||||
|
.dir = bullet_dir,
|
||||||
|
.speed = 50
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.drawRectangle(.{
|
||||||
|
.rect = self.player.getRect(),
|
||||||
|
.color = rgb(255, 255, 255)
|
||||||
|
});
|
||||||
|
|
||||||
|
for (self.bullets.items) |*bullet| {
|
||||||
|
bullet.kinetic.vel = bullet.dir.multiplyScalar(bullet.speed);
|
||||||
|
bullet.kinetic.update(dt, .{});
|
||||||
|
|
||||||
|
const bullet_rect = getCenteredRect(bullet.kinetic.pos, bullet.size);
|
||||||
|
|
||||||
|
for (self.enemies.items) |*enemy| {
|
||||||
|
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
||||||
|
if (enemy_rect.hasOverlap(bullet_rect)) {
|
||||||
|
enemy.dead = true;
|
||||||
|
bullet.dead = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.drawRectangle(.{
|
||||||
|
.rect = bullet_rect,
|
||||||
|
.color = rgb(200, 20, 255)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (self.enemies.items) |*enemy| {
|
||||||
|
const dir_to_player = self.player.kinetic.pos.sub(enemy.kinetic.pos).normalized();
|
||||||
|
enemy.kinetic.vel = dir_to_player.multiplyScalar(50);
|
||||||
|
enemy.kinetic.update(dt, .{});
|
||||||
|
|
||||||
|
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
||||||
|
|
||||||
|
if (enemy_rect.hasOverlap(self.player.getRect())) {
|
||||||
|
var is_invincible = false;
|
||||||
|
if (self.player.invincible_until) |invincible_until| {
|
||||||
|
is_invincible = frame.time_ns < invincible_until;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.player.health > 0 and !is_invincible) {
|
||||||
|
self.player.health -= 1;
|
||||||
|
|
||||||
|
var invincible_until = frame.time_ns;
|
||||||
|
invincible_until += @as(Nanoseconds, @intFromFloat(invincibility_duration_s * std.time.ns_per_s));
|
||||||
|
self.player.invincible_until = invincible_until;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.drawRectangle(.{
|
||||||
|
.rect = enemy_rect,
|
||||||
|
.color = rgb(20, 200, 20)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var index: usize = 0;
|
||||||
|
while (index < self.pickups.items.len) {
|
||||||
|
var destroy: bool = false;
|
||||||
|
const pickup = self.pickups.items[index];
|
||||||
|
|
||||||
|
const pickup_rect = Rect.initCentered(pickup.pos.x, pickup.pos.y, 10, 10);
|
||||||
|
|
||||||
|
if (pickup_rect.hasOverlap(self.player.getRect())) {
|
||||||
|
switch (pickup.kind) {
|
||||||
|
.money => {
|
||||||
|
self.player.money += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destroy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.drawRectangle(.{
|
||||||
|
.rect = pickup_rect,
|
||||||
|
.color = rgb(20, 20, 200)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (destroy) {
|
||||||
|
_ = self.pickups.swapRemove(index);
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < self.bullets.items.len) {
|
||||||
|
if (self.bullets.items[i].dead) {
|
||||||
|
_ = self.bullets.swapRemove(i);
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < self.enemies.items.len) {
|
||||||
|
if (self.enemies.items[i].dead) {
|
||||||
|
_ = self.enemies.swapRemove(i);
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text_opts = Engine.Frame.DrawTextOptions{
|
||||||
|
.font = self.assets.font_id.get(.regular)
|
||||||
|
};
|
||||||
|
frame.drawTextFormat(.init(10, 10), text_opts, "{d:02}:{d:02}", .{
|
||||||
|
@divFloor(wave_timer_s, 60),
|
||||||
|
@mod(wave_timer_s, 60)
|
||||||
|
});
|
||||||
|
|
||||||
|
frame.drawTextFormat(.init(10, 30), text_opts, "{d}", .{ self.player.money });
|
||||||
|
frame.drawTextFormat(.init(10, 50), text_opts, "{d}/{d}", .{ self.player.health, self.player.max_health });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getCenteredRect(pos: Vec2, size: f32) Rect {
|
||||||
|
return .{
|
||||||
|
.pos = pos.sub(Vec2.init(size, size).multiplyScalar(0.5)),
|
||||||
|
.size = .init(size, size),
|
||||||
|
};
|
||||||
|
}
|
||||||
433
src/game.zig
433
src/game.zig
@ -25,450 +25,47 @@ const Sprite = Engine.Graphics.Sprite;
|
|||||||
|
|
||||||
const RaycastTileIterator = @import("./raycast_tile_iterator.zig");
|
const RaycastTileIterator = @import("./raycast_tile_iterator.zig");
|
||||||
|
|
||||||
|
const CombatScreen = @import("./combat_screen.zig");
|
||||||
|
|
||||||
const Game = @This();
|
const Game = @This();
|
||||||
const world_size = Vec2.init(20 * 16, 15 * 16);
|
const world_size = Vec2.init(20 * 16, 15 * 16);
|
||||||
|
|
||||||
const RNGState = std.Random.DefaultPrng;
|
const RNGState = std.Random.DefaultPrng;
|
||||||
|
|
||||||
const Kinetic = struct {
|
|
||||||
pos: Vec2 = .zero,
|
|
||||||
vel: Vec2 = .zero,
|
|
||||||
acc: Vec2 = .zero,
|
|
||||||
|
|
||||||
const UpdateOptions = struct {
|
|
||||||
max_speed: ?f32 = null,
|
|
||||||
friction: ?f32 = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn update(self: *Kinetic, dt: f32, opts: UpdateOptions) void {
|
|
||||||
self.vel = self.vel.add(self.acc.multiplyScalar(dt));
|
|
||||||
if (opts.max_speed) |max_speed| {
|
|
||||||
self.vel = self.vel.limitLength(max_speed);
|
|
||||||
}
|
|
||||||
if (opts.friction) |friction| {
|
|
||||||
const friction_force = std.math.pow(f32, 1 - friction, dt);
|
|
||||||
self.vel = self.vel.multiplyScalar(friction_force);
|
|
||||||
}
|
|
||||||
self.pos = self.pos.add(self.vel.multiplyScalar(dt));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Player = struct {
|
|
||||||
kinetic: Kinetic = .{},
|
|
||||||
money: u32 = 0,
|
|
||||||
health: u32 = 0,
|
|
||||||
max_health: u32 = 0,
|
|
||||||
last_shot_at: ?Nanoseconds = null,
|
|
||||||
invincible_until: ?Nanoseconds = null,
|
|
||||||
|
|
||||||
pub fn getRect(self: Player) Rect {
|
|
||||||
return getCenteredRect(self.kinetic.pos, 16);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Bullet = struct {
|
|
||||||
kinetic: Kinetic,
|
|
||||||
size: f32 = 5,
|
|
||||||
dir: Vec2,
|
|
||||||
speed: f32,
|
|
||||||
|
|
||||||
dead: bool = false
|
|
||||||
};
|
|
||||||
|
|
||||||
const Enemy = struct {
|
|
||||||
kinetic: Kinetic,
|
|
||||||
speed: f32,
|
|
||||||
size: f32,
|
|
||||||
|
|
||||||
dead: bool = false
|
|
||||||
};
|
|
||||||
|
|
||||||
const Wave = struct {
|
|
||||||
started_at: Nanoseconds,
|
|
||||||
duration: Nanoseconds,
|
|
||||||
enemies_spawned: u32,
|
|
||||||
total_enemies: u32,
|
|
||||||
|
|
||||||
const Info = struct {
|
|
||||||
enemies: u32,
|
|
||||||
duration_s: u32,
|
|
||||||
starts_at_s: u32,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Pickup = struct {
|
|
||||||
const Kind = enum {
|
|
||||||
money
|
|
||||||
};
|
|
||||||
|
|
||||||
pos: Vec2,
|
|
||||||
kind: Kind,
|
|
||||||
};
|
|
||||||
|
|
||||||
const invincibility_duration_s = 0.5;
|
|
||||||
const pickup_spawn_duration_s = Range.init(1, 5);
|
|
||||||
const wave_infos = [_]Wave.Info{
|
|
||||||
.{
|
|
||||||
.enemies = 10,
|
|
||||||
.duration_s = 10,
|
|
||||||
.starts_at_s = 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
arena: std.heap.ArenaAllocator,
|
arena: std.heap.ArenaAllocator,
|
||||||
gpa: Allocator,
|
gpa: Allocator,
|
||||||
rng: RNGState,
|
|
||||||
assets: *Assets,
|
assets: *Assets,
|
||||||
|
|
||||||
player: Player = .{},
|
combat_screen: CombatScreen,
|
||||||
bullets: std.ArrayList(Bullet) = .empty,
|
|
||||||
enemies: std.ArrayList(Enemy) = .empty,
|
|
||||||
|
|
||||||
pickups: std.ArrayList(Pickup) = .empty,
|
|
||||||
next_pickup_spawn_at: Nanoseconds,
|
|
||||||
|
|
||||||
wave_timer: Nanoseconds = 0,
|
|
||||||
|
|
||||||
waves: std.ArrayList(Wave) = .empty,
|
|
||||||
spawned_waves: std.ArrayList(usize) = .empty,
|
|
||||||
|
|
||||||
pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game {
|
pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game {
|
||||||
var arena = std.heap.ArenaAllocator.init(gpa);
|
var arena = std.heap.ArenaAllocator.init(gpa);
|
||||||
errdefer arena.deinit();
|
errdefer arena.deinit();
|
||||||
|
|
||||||
var rng = RNGState.init(seed);
|
|
||||||
|
|
||||||
const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(rng.random());
|
|
||||||
const next_pickup_spawn_at: Nanoseconds = @intFromFloat(next_pickup_spawn_at_s * std.time.ns_per_s);
|
|
||||||
|
|
||||||
return Game{
|
return Game{
|
||||||
.arena = arena,
|
.arena = arena,
|
||||||
.gpa = gpa,
|
.gpa = gpa,
|
||||||
.assets = assets,
|
.assets = assets,
|
||||||
.next_pickup_spawn_at = next_pickup_spawn_at,
|
|
||||||
.player = .{
|
.combat_screen = .init(gpa, seed, assets),
|
||||||
.kinetic = .{
|
|
||||||
.pos = .init(50, 50),
|
|
||||||
},
|
|
||||||
.health = 3,
|
|
||||||
.max_health = 3
|
|
||||||
},
|
|
||||||
.rng = rng
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Game) void {
|
pub fn deinit(self: *Game) void {
|
||||||
self.arena.deinit();
|
self.arena.deinit();
|
||||||
self.bullets.deinit(self.gpa);
|
self.combat_screen.deinit();
|
||||||
self.enemies.deinit(self.gpa);
|
|
||||||
self.waves.deinit(self.gpa);
|
|
||||||
self.pickups.deinit(self.gpa);
|
|
||||||
self.spawned_waves.deinit(self.gpa);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn getCenteredRect(pos: Vec2, size: f32) Rect {
|
|
||||||
return .{
|
|
||||||
.pos = pos.sub(Vec2.init(size, size).multiplyScalar(0.5)),
|
|
||||||
.size = .init(size, size),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawnEnemy(self: *Game) !void {
|
|
||||||
const spawn_area_margin = 20;
|
|
||||||
const spawn_area_size = 10;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const spawn_areas = [_]Rect{
|
|
||||||
top_spawn_area,
|
|
||||||
bottom_spawn_area,
|
|
||||||
left_spawn_area,
|
|
||||||
right_spawn_area
|
|
||||||
};
|
|
||||||
|
|
||||||
const rand = self.rng.random();
|
|
||||||
const spawn_area = spawn_areas[rand.uintLessThan(usize, spawn_areas.len)];
|
|
||||||
const pos = Vec2.initRandomRect(rand, spawn_area);
|
|
||||||
|
|
||||||
try self.enemies.append(self.gpa, .{
|
|
||||||
.kinetic = .{ .pos = pos },
|
|
||||||
.speed = 10,
|
|
||||||
.size = 20
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawnPickup(self: *Game) !void {
|
|
||||||
const margin = 10;
|
|
||||||
const spawn_area = (Rect{ .pos = .zero, .size = world_size }).grow(-margin);
|
|
||||||
const pos = Vec2.initRandomRect(self.rng.random(), spawn_area);
|
|
||||||
|
|
||||||
try self.pickups.append(self.gpa, Pickup{
|
|
||||||
.pos = pos,
|
|
||||||
.kind = .money
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(self: *Game, frame: *Engine.Frame) !void {
|
pub fn tick(self: *Game, frame: *Engine.Frame) !void {
|
||||||
const dt = frame.deltaTime();
|
if (frame.isKeyPressed(.ESCAPE)) {
|
||||||
|
sapp.requestQuit();
|
||||||
self.wave_timer += frame.dt_ns;
|
|
||||||
const wave_timer_s = @divFloor(self.wave_timer, std.time.ns_per_s);
|
|
||||||
|
|
||||||
for (0.., wave_infos) |i, wave_info| {
|
|
||||||
if (std.mem.indexOfScalar(usize, self.spawned_waves.items, i) != null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wave_info.starts_at_s > wave_timer_s) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.spawned_waves.append(self.gpa, i);
|
|
||||||
try self.waves.append(self.gpa, .{
|
|
||||||
.started_at = self.wave_timer,
|
|
||||||
.duration = @as(u64, wave_info.duration_s) * std.time.ns_per_s,
|
|
||||||
.enemies_spawned = 0,
|
|
||||||
.total_enemies = wave_info.enemies
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
var index: usize = 0;
|
|
||||||
while (index < self.waves.items.len) {
|
|
||||||
var wave_complete = false;
|
|
||||||
const wave = &self.waves.items[index];
|
|
||||||
|
|
||||||
const wave_time_passed: f32 = @floatFromInt(self.wave_timer - wave.started_at);
|
|
||||||
const wave_duration: f32 = @floatFromInt(wave.duration);
|
|
||||||
const percent_complete = std.math.clamp(wave_time_passed / wave_duration, 0, 1);
|
|
||||||
const wave_total_enemies: f32 = @floatFromInt(wave.total_enemies);
|
|
||||||
const expected_enemies: u32 = @intFromFloat(wave_total_enemies * percent_complete);
|
|
||||||
|
|
||||||
while (wave.enemies_spawned < expected_enemies) {
|
|
||||||
try self.spawnEnemy();
|
|
||||||
wave.enemies_spawned += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wave.enemies_spawned == wave.total_enemies) {
|
|
||||||
wave_complete = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wave_complete) {
|
|
||||||
_ = self.waves.swapRemove(index);
|
|
||||||
} else {
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.wave_timer >= self.next_pickup_spawn_at) {
|
|
||||||
const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(self.rng.random());
|
|
||||||
const next_pickup_spawn_at: Nanoseconds = @intFromFloat(next_pickup_spawn_at_s * std.time.ns_per_s);
|
|
||||||
self.next_pickup_spawn_at = self.wave_timer + next_pickup_spawn_at;
|
|
||||||
try self.spawnPickup();
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.graphics.canvas_size = world_size;
|
|
||||||
|
|
||||||
if (frame.isKeyPressed(.F3)) {
|
if (frame.isKeyPressed(.F3)) {
|
||||||
frame.show_debug = !frame.show_debug;
|
frame.show_debug = !frame.show_debug;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame.isKeyPressed(.ESCAPE)) {
|
try self.combat_screen.tick(frame);
|
||||||
sapp.requestQuit();
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.drawRectangle(.{
|
|
||||||
.rect = .init(0, 0, world_size.x, world_size.y),
|
|
||||||
.color = rgb(20, 20, 20)
|
|
||||||
});
|
|
||||||
|
|
||||||
var dir = Vec2.init(0, 0);
|
|
||||||
if (frame.isKeyDown(.W)) {
|
|
||||||
dir.y -= 1;
|
|
||||||
}
|
|
||||||
if (frame.isKeyDown(.S)) {
|
|
||||||
dir.y += 1;
|
|
||||||
}
|
|
||||||
if (frame.isKeyDown(.A)) {
|
|
||||||
dir.x -= 1;
|
|
||||||
}
|
|
||||||
if (frame.isKeyDown(.D)) {
|
|
||||||
dir.x += 1;
|
|
||||||
}
|
|
||||||
dir = dir.normalized();
|
|
||||||
|
|
||||||
const acceleration = 1500;
|
|
||||||
|
|
||||||
self.player.kinetic.acc = dir.multiplyScalar(acceleration);
|
|
||||||
self.player.kinetic.update(dt, .{
|
|
||||||
.friction = 0.99988,
|
|
||||||
.max_speed = 400
|
|
||||||
});
|
|
||||||
|
|
||||||
if (frame.input.mouse_position) |mouse| {
|
|
||||||
const cursor_size = Vec2.init(10, 10);
|
|
||||||
frame.drawRectangle(.{
|
|
||||||
.rect = .{
|
|
||||||
.pos = mouse.sub(cursor_size.divideScalar(2)),
|
|
||||||
.size = cursor_size,
|
|
||||||
},
|
|
||||||
.color = rgba(255, 200, 200, 0.5)
|
|
||||||
});
|
|
||||||
|
|
||||||
const bullet_dir = mouse.sub(self.player.kinetic.pos).normalized();
|
|
||||||
|
|
||||||
var cooldown_complete = true;
|
|
||||||
if (self.player.last_shot_at) |last_shot_at| {
|
|
||||||
const cooldown_ns = std.time.ns_per_ms * 500;
|
|
||||||
cooldown_complete = frame.time_ns > last_shot_at + cooldown_ns;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame.isMouseDown(.left) and cooldown_complete) {
|
|
||||||
self.player.last_shot_at = frame.time_ns;
|
|
||||||
try self.bullets.append(self.gpa, .{
|
|
||||||
.kinetic = .{
|
|
||||||
.pos = self.player.kinetic.pos,
|
|
||||||
},
|
|
||||||
.dir = bullet_dir,
|
|
||||||
.speed = 50
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.drawRectangle(.{
|
|
||||||
.rect = self.player.getRect(),
|
|
||||||
.color = rgb(255, 255, 255)
|
|
||||||
});
|
|
||||||
|
|
||||||
for (self.bullets.items) |*bullet| {
|
|
||||||
bullet.kinetic.vel = bullet.dir.multiplyScalar(bullet.speed);
|
|
||||||
bullet.kinetic.update(dt, .{});
|
|
||||||
|
|
||||||
const bullet_rect = getCenteredRect(bullet.kinetic.pos, bullet.size);
|
|
||||||
|
|
||||||
for (self.enemies.items) |*enemy| {
|
|
||||||
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
|
||||||
if (enemy_rect.hasOverlap(bullet_rect)) {
|
|
||||||
enemy.dead = true;
|
|
||||||
bullet.dead = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.drawRectangle(.{
|
|
||||||
.rect = bullet_rect,
|
|
||||||
.color = rgb(200, 20, 255)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (self.enemies.items) |*enemy| {
|
|
||||||
const dir_to_player = self.player.kinetic.pos.sub(enemy.kinetic.pos).normalized();
|
|
||||||
enemy.kinetic.vel = dir_to_player.multiplyScalar(50);
|
|
||||||
enemy.kinetic.update(dt, .{});
|
|
||||||
|
|
||||||
const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size);
|
|
||||||
|
|
||||||
if (enemy_rect.hasOverlap(self.player.getRect())) {
|
|
||||||
var is_invincible = false;
|
|
||||||
if (self.player.invincible_until) |invincible_until| {
|
|
||||||
is_invincible = frame.time_ns < invincible_until;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.player.health > 0 and !is_invincible) {
|
|
||||||
self.player.health -= 1;
|
|
||||||
|
|
||||||
var invincible_until = frame.time_ns;
|
|
||||||
invincible_until += @as(Nanoseconds, @intFromFloat(invincibility_duration_s * std.time.ns_per_s));
|
|
||||||
self.player.invincible_until = invincible_until;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.drawRectangle(.{
|
|
||||||
.rect = enemy_rect,
|
|
||||||
.color = rgb(20, 200, 20)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var index: usize = 0;
|
|
||||||
while (index < self.pickups.items.len) {
|
|
||||||
var destroy: bool = false;
|
|
||||||
const pickup = self.pickups.items[index];
|
|
||||||
|
|
||||||
const pickup_rect = Rect.initCentered(pickup.pos.x, pickup.pos.y, 10, 10);
|
|
||||||
|
|
||||||
if (pickup_rect.hasOverlap(self.player.getRect())) {
|
|
||||||
switch (pickup.kind) {
|
|
||||||
.money => {
|
|
||||||
self.player.money += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
destroy = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.drawRectangle(.{
|
|
||||||
.rect = pickup_rect,
|
|
||||||
.color = rgb(20, 20, 200)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (destroy) {
|
|
||||||
_ = self.pickups.swapRemove(index);
|
|
||||||
} else {
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < self.bullets.items.len) {
|
|
||||||
if (self.bullets.items[i].dead) {
|
|
||||||
_ = self.bullets.swapRemove(i);
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var i: usize = 0;
|
|
||||||
while (i < self.enemies.items.len) {
|
|
||||||
if (self.enemies.items[i].dead) {
|
|
||||||
_ = self.enemies.swapRemove(i);
|
|
||||||
} else {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const text_opts = Engine.Frame.DrawTextOptions{
|
|
||||||
.font = self.assets.font_id.get(.regular)
|
|
||||||
};
|
|
||||||
frame.drawTextFormat(.init(10, 10), text_opts, "{d:02}:{d:02}", .{
|
|
||||||
@divFloor(wave_timer_s, 60),
|
|
||||||
@mod(wave_timer_s, 60)
|
|
||||||
});
|
|
||||||
|
|
||||||
frame.drawTextFormat(.init(10, 30), text_opts, "{d}", .{ self.player.money });
|
|
||||||
frame.drawTextFormat(.init(10, 50), text_opts, "{d}/{d}", .{ self.player.health, self.player.max_health });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug(self: *Game) !void {
|
pub fn debug(self: *Game) !void {
|
||||||
@ -482,13 +79,15 @@ pub fn debug(self: *Game) !void {
|
|||||||
defer imgui.endWindow();
|
defer imgui.endWindow();
|
||||||
|
|
||||||
if (imgui.button("Spawn enemy")) {
|
if (imgui.button("Spawn enemy")) {
|
||||||
try self.spawnEnemy();
|
try self.combat_screen.spawnEnemy();
|
||||||
}
|
}
|
||||||
|
|
||||||
const time_left_til_pickup = self.next_pickup_spawn_at - self.wave_timer;
|
const screen = &self.combat_screen;
|
||||||
|
|
||||||
imgui.textFmt("Waves: {}\n", .{self.waves.items.len});
|
const time_left_til_pickup = screen.next_pickup_spawn_at - screen.wave_timer;
|
||||||
imgui.textFmt("Bullets: {}\n", .{self.bullets.items.len});
|
|
||||||
imgui.textFmt("Enemies: {}\n", .{self.enemies.items.len});
|
imgui.textFmt("Waves: {}\n", .{screen.waves.items.len});
|
||||||
|
imgui.textFmt("Bullets: {}\n", .{screen.bullets.items.len});
|
||||||
|
imgui.textFmt("Enemies: {}\n", .{screen.enemies.items.len});
|
||||||
imgui.textFmt("Time until next pickup: {d:.2}s\n", .{@as(f32, @floatFromInt(time_left_til_pickup)) / std.time.ns_per_s});
|
imgui.textFmt("Time until next pickup: {d:.2}s\n", .{@as(f32, @floatFromInt(time_left_til_pickup)) / std.time.ns_per_s});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user