Files
cpu-vs-ai/src/main.zig
2025-07-27 22:47:35 +03:00

1963 lines
71 KiB
Zig

const std = @import("std");
const rl = @import("raylib");
const rlg = @import("raygui");
const sin = std.math.sin;
const cos = std.math.cos;
const assert = std.debug.assert;
const cell_size = 32;
const cells_width = 16.0;
const cells_height = 9.0;
const world_width = cells_width * cell_size;
const world_height = cells_height * cell_size;
const bg_color = rl.Color.init(0x20, 0x2e, 0x37, 0xFF);
const highlight_color = rl.Color.init(0x57, 0x72, 0x77, 0xFF);
const debug = false;
const edit = debug and false;
var screen_width: i32 = 1280;
var screen_height: i32 = 720;
const konami_code = [_]rl.KeyboardKey{
.up,
.up,
.down,
.down,
.left,
.right,
.left,
.right,
.b,
.a,
.enter,
};
// TODO: ideas for the future (after hackathon)
// "Heat system" (shooting lazers increases heat, heat decreases over time, if heat is too high, CPU throttles)
// "Cooling system" some way to have CPUs that are more resistant to heat
pub fn main() anyerror!void {
// Initialization
//--------------------------------------------------------------------------------------
rl.setConfigFlags(.{ .window_resizable = false, .window_highdpi = true });
rl.initWindow(screen_width, screen_height, "CPU vs AI");
defer rl.closeWindow(); // Close window and OpenGL context
rl.initAudioDevice();
defer rl.closeAudioDevice();
var game = Game.init();
defer game.deinit();
const music = rl.loadMusicStream("assets/sfx/music.wav") catch unreachable;
rl.setMusicVolume(music, 0.5);
rl.playMusicStream(music);
rl.setWindowIcon(rl.loadImage("assets/img/icon.png") catch unreachable);
rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second
rl.setExitKey(.caps_lock);
//--------------------------------------------------------------------------------------
// Main game loop
while (!rl.windowShouldClose()) { // Detect window close button or ESC key
// Update
//----------------------------------------------------------------------------------
rl.updateMusicStream(music);
game.process();
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
game.render();
//----------------------------------------------------------------------------------
}
}
/// Gets the mouse position, scaled by camera zoom and clamped to fix issues on MacOS
fn getMousePos(camera: *const rl.Camera2D) rl.Vector2 {
const msp = rl.getMousePosition().scale(1 / camera.zoom);
return .{
.x = @max(msp.x, 0),
.y = @max(msp.y, 0),
};
}
fn readCode(counter: *u8, seq: []const rl.KeyboardKey) bool {
if (counter.* >= seq.len) {
counter.* = 0;
return true;
} else if (rl.isKeyPressed(seq[@as(usize, counter.*)])) {
counter.* += 1;
}
return false;
}
fn textureButtonScaled(game: *Game, camera: *const rl.Camera2D, texture: rl.Texture2D, pos: rl.Vector2, rotation: f32, scale: f32) bool {
const msp = getMousePos(camera);
const rect = rl.Rectangle.init(pos.x, pos.y, cell_size * scale, cell_size * scale);
const ms_rect = rl.Rectangle.init(msp.x, msp.y, 1, 1);
const hovered = rect.checkCollision(ms_rect);
const clicked = hovered and rl.isMouseButtonPressed(.left);
const offset: f32 = if (hovered) 1 else 0;
rl.drawTexturePro(
texture,
.{ .x = offset * cell_size, .y = 0, .width = cell_size, .height = cell_size },
rect,
rl.Vector2.zero(),
rotation,
rl.Color.white,
);
if (clicked) {
rl.playSound(game.sound_map.get(.button_click).?);
}
return clicked;
}
fn labelButton(
game: *Game,
camera: *const rl.Camera2D,
text: [:0]const u8,
rect: rl.Rectangle,
opt: struct {
font: rl.Font,
font_size: f32,
spacing: f32,
text_color: rl.Color = rl.Color.white,
},
) bool {
const msp = getMousePos(camera);
const texture = game.texture_map.get(.button).?;
const ms_rect = rl.Rectangle.init(msp.x, msp.y, 1, 1);
const hovered = rect.checkCollision(ms_rect);
const mouse_down = rl.isMouseButtonDown(.left);
const clicked = hovered and rl.isMouseButtonReleased(.left);
const offset: f32 = if (mouse_down) 2 else if (hovered) 1 else 0;
assert(rect.width >= cell_size * 2); // Button needs at least 2 cells!
assert(@mod(rect.width, cell_size) == 0); // Needs to be a multiple of cell size
assert(rect.height == cell_size); // Needs to be a single row
rl.drawTexturePro(
texture,
.{ .x = cell_size * 3 * offset, .y = 0, .width = cell_size, .height = cell_size },
.{ .x = rect.x, .y = rect.y, .width = cell_size, .height = rect.height },
rl.Vector2.zero(),
0,
rl.Color.white,
);
rl.drawTexturePro(
texture,
.{ .x = cell_size * 3 * offset + cell_size, .y = 0, .width = cell_size, .height = cell_size },
.{ .x = rect.x + cell_size, .y = rect.y, .width = rect.width - cell_size, .height = rect.height },
rl.Vector2.zero(),
0,
rl.Color.white,
);
rl.drawTexturePro(
texture,
.{ .x = cell_size * 3 * offset + (cell_size * 2), .y = 0, .width = cell_size, .height = cell_size },
.{ .x = rect.x + rect.width - cell_size, .y = rect.y, .width = cell_size, .height = rect.height },
rl.Vector2.zero(),
0,
rl.Color.white,
);
const text_vec = rl.measureTextEx(opt.font, text, opt.font_size, opt.spacing);
rl.drawTextEx(
opt.font,
text,
rl.Vector2.init(rect.x + ((rect.width - text_vec.x) / 2), rect.y + ((rect.height - text_vec.y) / 2)),
opt.font_size,
opt.spacing,
opt.text_color,
);
if (mouse_down) {
rl.playSound(game.sound_map.get(.button_click).?);
}
return clicked;
}
const BugKind = enum(u8) {
nullptr_deref = 0,
stack_overflow = 1,
infinite_loop = 2,
const count = @typeInfo(BugKind).@"enum".fields.len;
};
const Bug = struct {
kind: BugKind,
health: f32,
position: rl.Vector2,
previous: rl.Vector2, // Center of prev
target: rl.Vector2, // Center of next cell
animation_time: f32 = 0,
animation_state: f32 = 0,
prev_angle: f32 = 0,
angle: f32 = 0,
dead: bool = false,
pub fn init(kind: BugKind, position: rl.Vector2) Bug {
return Bug{
.kind = kind,
.health = maxHealth(kind),
.position = position,
.previous = position,
.target = position.add(rl.Vector2.init(cell_size, 0)), // 1 unit to the right
};
}
pub fn update(self: *Bug, delta_time: f32, wave: *Wave, ram: *f32, game: *Game) void {
self.animation_time += delta_time;
if (self.animation_time > animationSwitchThreshold(self.kind)) {
self.animation_time = 0;
self.animation_state = @mod((self.animation_state + 1), animationCount(self.kind));
}
var velocity: rl.Vector2 = self.target.subtract(self.position);
velocity = velocity.normalize().scale(speed(self.kind));
self.position = self.position.add(velocity);
const prev_grid_x: usize = @intFromFloat(self.previous.x / cell_size);
const prev_grid_y: usize = @intFromFloat(self.previous.y / cell_size);
const grid_x: usize = @intFromFloat(self.position.x / cell_size);
const grid_y: usize = @intFromFloat(self.position.y / cell_size);
const target_grid_x: usize = @intFromFloat(self.target.x / cell_size);
const target_grid_y: usize = @intFromFloat(self.target.y / cell_size);
const c_lane_left = wave.get(grid_x -| 1, grid_y).isLaneConnected();
const c_lane_right = wave.get(grid_x + 1, grid_y).isLaneConnected();
const c_lane_top = wave.get(grid_x, grid_y -| 1).isLaneConnected();
const c_lane_bottom = wave.get(grid_x, grid_y + 1).isLaneConnected();
const matching = grid_x == target_grid_x and grid_y == target_grid_y;
const prev_angle = self.prev_angle;
// For animation rotation
var desired_angle_delta: f32 = 0.0;
if (c_lane_left and c_lane_bottom and !matching) {
desired_angle_delta = 90;
} else if (c_lane_right and c_lane_top and !matching) {
desired_angle_delta = -90;
} else if (c_lane_left and c_lane_top and !matching) {
desired_angle_delta = -90;
} else if (c_lane_right and c_lane_bottom and !matching) {
desired_angle_delta = 90;
}
const distance = cell_size / 2;
// const t = if (matching)
// self.position.distance(self.target) / distance
// else
// self.position.distance(self.previous) / distance;
const t = self.position.distance(self.previous) / distance;
const desired_angle = prev_angle + desired_angle_delta;
if (desired_angle_delta != 0) {
// self.angle = rl.math.lerp(@min(prev_angle, desired_angle), @max(prev_angle, desired_angle), t);
self.angle = rl.math.lerp(prev_angle, desired_angle, t);
if (t > 0.99) {
// self.prev_angle = self.angle;
self.prev_angle = @round(self.angle / 90.0) * 90.0;
}
std.debug.print("------------------------\n", .{});
std.debug.print("angle: {d}\n", .{self.angle});
std.debug.print("t: {d}\n", .{t});
std.debug.print("desired_angle_delta: {d}\n", .{desired_angle_delta});
std.debug.print("desired_angle: {d}\n", .{desired_angle});
std.debug.print("prev_angle: {d}\n", .{prev_angle});
std.debug.print("self.prev_angle: {d}\n", .{self.prev_angle});
}
// 1 is 1/32th of a cell
if (self.position.distanceSqr(self.target) < 1) {
// Find new target
const t_lane_left = wave.get(target_grid_x - 1, target_grid_y).isLaneConnected();
const t_lane_right = wave.get(target_grid_x + 1, target_grid_y).isLaneConnected();
const t_lane_top = wave.get(target_grid_x, target_grid_y - 1).isLaneConnected();
const t_lane_bottom = wave.get(target_grid_x, target_grid_y + 1).isLaneConnected();
var target: rl.Vector2 = rl.Vector2.init(0, 0);
if (t_lane_left and prev_grid_x != target_grid_x - 1) {
target = self.target.add(rl.Vector2.init(-cell_size, 0));
std.debug.print("lane left\n", .{});
}
if (t_lane_right and prev_grid_x != target_grid_x + 1) {
target = self.target.add(rl.Vector2.init(cell_size, 0));
std.debug.print("lane right\n", .{});
}
if (t_lane_top and prev_grid_y != target_grid_y - 1) {
target = self.target.add(rl.Vector2.init(0, -cell_size));
std.debug.print("lane top\n", .{});
}
if (t_lane_bottom and prev_grid_y != target_grid_y + 1) {
target = self.target.add(rl.Vector2.init(0, cell_size));
std.debug.print("lane bottom\n", .{});
}
if (target.x == 0 and target.y == 0) {
if (wave.get(target_grid_x, target_grid_y) == .ram) {
self.dead = true;
const prev_ram_percentage = ram.* / ScreenBattle.max_ram;
ram.* = @max(ram.* - self.memory(), 0);
const ram_percentage = ram.* / ScreenBattle.max_ram;
const is_75 = prev_ram_percentage > 0.75 and ram_percentage <= 0.75;
const is_50 = prev_ram_percentage > 0.5 and ram_percentage <= 0.5;
const is_25 = prev_ram_percentage > 0.25 and ram_percentage <= 0.25;
const is_0 = prev_ram_percentage > 0 and ram_percentage <= 0;
if (is_75 or is_50 or is_25 or is_0) {
rl.playSound(game.sound_map.get(.ram_destroyed).?);
} else {
rl.playSound(game.sound_map.get(.bug_attack).?);
}
} else {
unreachable;
}
}
self.previous = self.target;
self.target = target;
}
}
fn transistors(bug: Bug) u32 {
return switch (bug.kind) {
.nullptr_deref => return 10,
.stack_overflow => return 20,
.infinite_loop => return 5,
};
}
pub fn maxHealth(kind: BugKind) f32 {
return switch (kind) {
.nullptr_deref => return 2,
.stack_overflow => return 10,
.infinite_loop => return 0.5,
};
}
fn speed(kind: BugKind) f32 {
return switch (kind) {
.nullptr_deref => return 0.5,
.stack_overflow => return 0.1,
.infinite_loop => return 4,
};
}
fn damage(self: *Bug, dmg: f32) bool {
self.health -= dmg;
if (self.health <= 0) {
self.dead = true;
}
return self.dead;
}
fn memory(self: Bug) f32 {
return switch (self.kind) {
.nullptr_deref => return 10,
.stack_overflow => return 20,
.infinite_loop => return 6,
// .nullptr_deref => return 1,
// .stack_overflow => return 2,
// .infinite_loop => return 0.5,
} * self.health;
}
fn animationCount(kind: BugKind) f32 {
return switch (kind) {
.nullptr_deref => return 2,
.stack_overflow => return 2,
.infinite_loop => return 6,
};
}
fn animationSwitchThreshold(kind: BugKind) f32 {
return (1 / speed(kind)) / (animationCount(kind) * 3);
// return switch (kind) {
// .nullptr_deref => return 0.3,
// .stack_overflow => return 0.3,
// .infinite_loop => return 0.05,
// };
}
};
const Instruction = struct {
condition: Condition = .always,
opcode: Opcode = .noop,
const Condition = union(enum) {
always,
memory_leak: f32, // Health percentage, ram<0.25 or ram<0.5 (randomized)
bug: BugKind, // Against this specific enemy type
idle: f32, // Idle time in seconds, 1s-2s (randomized)
};
const Opcode = union(enum) {
noop, // does nothing
sleep: f32, // Slows enemies (multiplier of enemy speed, 0.5-0.9 randomized)
prefetch: f32, // Larger radius (multiplier of cache size, 1.5-2 randomized)
overclock: f32, // Deals more damage (multiplier of bus width, 1.5-2 randomized)
// // overclock: f32, // Faster firerate (multiplier of clock speed, 1.1-1.5 randomized)
};
};
const Cpu = struct {
// clock_speed: f32 = 1, // Fire rate, every how many seconds to fire
bus_width: f32 = 1, // Bus width, damage dealt
cache_size: f32 = 1.5, // Cache size, range (distance for attack)
debugs: u32 = 0, // How many bugs were killed
instructions: [max_instructions]Instruction = [_]Instruction{.{}} ** max_instructions, // modifiers
upgrades: u32 = 1, // TODO: remove this after hackathon, for a better instruction system
const max_instructions = 5;
fn damage(self: Cpu) f32 {
return self.bus_width;
}
fn cores(self: Cpu) u32 {
return switch (self.level()) {
1 => 1,
2 => 2,
3 => 4,
4 => 8,
else => 16,
};
}
fn level(self: Cpu) u32 {
return switch (self.debugs) {
// TODO: only for debug (change this once ready for play testing)
// 0...2 => 1,
// 3...4 => 2,
// 5...6 => 3,
// 7...8 => 4,
// else => 5,
0...9 => 1,
10...24 => 2,
25...74 => 3,
75...250 => 4,
else => 5,
};
}
fn upgradeBusWidth(self: *Cpu) bool {
if (self.upgrades < self.level()) {
self.upgrades += 1;
self.bus_width += 1;
return true;
}
return false;
}
fn upgradeCacheSize(self: *Cpu) bool {
if (self.upgrades < self.level()) {
self.upgrades += 1;
self.cache_size += 0.5;
return true;
}
return false;
}
fn hasUpgrades(self: Cpu) bool {
return self.upgrades < self.level();
}
};
const Cell = union(enum) {
none,
socket,
cpu: Cpu,
ai,
ram,
lane,
fn isLaneConnected(self: Cell) bool {
return switch (self) {
.lane => true,
.ai => true,
.ram => true,
else => false,
};
}
};
const SpawnRule = struct {
from_time_s: f32,
to_time_s: f32,
bugs: [BugKind.count]struct {
last_spawn: f32 = 0,
spawn_interval: f32,
}, // index is enum
};
const Wave = struct {
arena: std.heap.ArenaAllocator,
map: [cells_height][cells_width]Cell = [_][cells_width]Cell{[_]Cell{.none} ** cells_width} ** cells_height,
bugs: std.ArrayList(Bug),
spawn_rules: std.ArrayList(SpawnRule),
time_since_start: f32 = 0,
fn init(wave_number: u8) Wave {
const height_middle = 4;
// TODO: probably fine for hackathon but post hackathon, this is really bad
// We alloc 2 arraylists on this, we need a pool allocator
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
// This is horrible code, sorry
var spawn_rules = std.ArrayList(SpawnRule).init(arena.allocator());
var map: [cells_height][cells_width]Cell = undefined;
if (wave_number == 1) {
map = [_][cells_width]Cell{
.{ .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none },
.{ .none, .none, .none, .none, .none, .socket, .socket, .socket, .socket, .socket, .none, .none, .none, .none, .none, .none },
.{ .none, .none, .none, .none, .none, .socket, .lane, .lane, .lane, .socket, .none, .none, .none, .none, .none, .none },
.{ .none, .socket, .socket, .socket, .socket, .socket, .lane, .socket, .lane, .socket, .socket, .socket, .socket, .socket, .socket, .none },
.{ .ai, .lane, .lane, .lane, .lane, .socket, .lane, .socket, .lane, .lane, .lane, .lane, .lane, .lane, .lane, .ram },
.{ .none, .socket, .socket, .socket, .lane, .socket, .lane, .socket, .socket, .socket, .socket, .socket, .socket, .socket, .socket, .none },
.{ .none, .none, .none, .socket, .lane, .lane, .lane, .socket, .none, .none, .none, .none, .none, .none, .none, .none },
.{ .none, .none, .none, .socket, .socket, .socket, .socket, .socket, .none, .none, .none, .none, .none, .none, .none, .none },
.{ .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none },
};
spawn_rules.append(.{
.from_time_s = 0,
.to_time_s = 4,
.bugs = .{
.{ .spawn_interval = 3 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 7,
.to_time_s = 20,
.bugs = .{
.{ .spawn_interval = 2.5 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 24,
.to_time_s = 32,
.bugs = .{
.{ .spawn_interval = 2 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 35,
.to_time_s = 36.1,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 1 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 40,
.to_time_s = 65,
.bugs = .{
.{ .spawn_interval = 1.2 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 60,
.to_time_s = 60.15,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0.1 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 60,
.to_time_s = 71,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 2 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 65,
.to_time_s = 75,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 4 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
} else if (wave_number == 2) {
spawn_rules.append(.{
.from_time_s = 0,
.to_time_s = 40,
.bugs = .{
.{ .spawn_interval = 2 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 20,
.to_time_s = 60,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 3 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 20,
.to_time_s = 80,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 5 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 30,
.to_time_s = 80,
.bugs = .{
.{ .spawn_interval = 0.8 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 50,
.to_time_s = 120,
.bugs = .{
.{ .spawn_interval = 0.5 },
.{ .spawn_interval = 2 },
.{ .spawn_interval = 1 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 80,
.to_time_s = 120,
.bugs = .{
.{ .spawn_interval = 0.5 },
.{ .spawn_interval = 1 },
.{ .spawn_interval = 0.5 },
},
}) catch unreachable;
map = .{
.{ .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none },
.{ .none, .none, .none, .socket, .socket, .socket, .socket, .socket, .socket, .socket, .none, .none, .none, .none, .none, .none },
.{ .none, .none, .none, .socket, .lane, .lane, .lane, .lane, .lane, .socket, .none, .none, .none, .none, .none, .none },
.{ .none, .socket, .socket, .socket, .lane, .socket, .socket, .socket, .lane, .socket, .socket, .socket, .socket, .socket, .socket, .none },
.{ .ai, .lane, .lane, .lane, .lane, .socket, .lane, .lane, .lane, .socket, .lane, .lane, .lane, .lane, .lane, .ram },
.{ .none, .socket, .socket, .socket, .socket, .socket, .lane, .socket, .socket, .socket, .lane, .socket, .socket, .socket, .socket, .none },
.{ .none, .none, .none, .none, .none, .socket, .lane, .lane, .lane, .lane, .lane, .socket, .none, .none, .none, .none },
.{ .none, .none, .none, .none, .none, .socket, .socket, .socket, .socket, .socket, .socket, .socket, .none, .none, .none, .none },
.{ .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none },
};
} else if (wave_number == 3) {
spawn_rules.append(.{
.from_time_s = 0,
.to_time_s = 3,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0.9 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 3,
.to_time_s = 40,
.bugs = .{
.{ .spawn_interval = 1.8 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 30,
.to_time_s = 43,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 3 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 20,
.to_time_s = 40,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 5 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 40,
.to_time_s = 41,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0.03 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 40,
.to_time_s = 100,
.bugs = .{
.{ .spawn_interval = 0.5 },
.{ .spawn_interval = 1 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 70,
.to_time_s = 71,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0.01 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 80,
.to_time_s = 82,
.bugs = .{
.{ .spawn_interval = 0.1 },
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
spawn_rules.append(.{
.from_time_s = 87,
.to_time_s = 88,
.bugs = .{
.{ .spawn_interval = 0 },
.{ .spawn_interval = 0.1 },
.{ .spawn_interval = 0 },
},
}) catch unreachable;
map = .{
.{ .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none },
.{ .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none },
.{ .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none },
.{ .none, .socket, .socket, .socket, .socket, .socket, .none, .none, .none, .none, .none, .none, .socket, .socket, .socket, .none },
.{ .ai, .lane, .lane, .lane, .lane, .socket, .socket, .socket, .socket, .socket, .socket, .socket, .socket, .lane, .lane, .ram },
.{ .none, .socket, .socket, .socket, .lane, .socket, .socket, .lane, .lane, .lane, .lane, .lane, .socket, .lane, .socket, .none },
.{ .none, .none, .none, .socket, .lane, .lane, .lane, .lane, .socket, .socket, .socket, .lane, .lane, .lane, .socket, .none },
.{ .none, .none, .none, .socket, .socket, .socket, .socket, .socket, .socket, .none, .socket, .socket, .socket, .socket, .socket, .none },
.{ .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none, .none },
};
} else {
unreachable;
}
var wave = Wave{
.arena = arena,
.bugs = std.ArrayList(Bug).init(arena.allocator()),
.spawn_rules = spawn_rules,
.map = map,
};
wave.map[height_middle][0] = .ai;
wave.map[height_middle][cells_width - 1] = .ram;
return wave;
}
fn update(self: *Wave, delta_time: f32, ram: *f32, game: *Game) bool {
// TODO: quite a bit of logic here
self.time_since_start += delta_time;
const half_cell = cell_size / 2;
const ai_position = rl.Vector2.init(half_cell, 4 * cell_size + half_cell);
var wave_over = true;
// TODO: post hackathon, optimize this
for (self.spawn_rules.items) |*rules| {
if (rules.to_time_s < self.time_since_start) {
continue;
}
wave_over = false;
if (rules.from_time_s > self.time_since_start) {
continue;
}
for (0..rules.bugs.len) |i| {
if (rules.bugs[i].spawn_interval == 0) {
continue;
}
rules.bugs[i].last_spawn += delta_time;
if (rules.bugs[i].last_spawn >= rules.bugs[i].spawn_interval) {
rules.bugs[i].last_spawn -= rules.bugs[i].spawn_interval;
self.bugs.append(Bug.init(@enumFromInt(i), ai_position)) catch unreachable;
}
}
}
for (self.bugs.items) |*bug| {
if (bug.dead) {
continue;
}
wave_over = false;
bug.update(delta_time, self, ram, game);
}
return wave_over;
}
fn deinit(self: *Wave) void {
self.arena.deinit();
}
fn at(self: *Wave, x: usize, y: usize) ?*Cell {
if (x >= cells_width or y >= cells_height or x < 0 or y < 0) {
return null;
}
return &self.map[y][x];
}
fn get(self: *Wave, x: usize, y: usize) Cell {
if (x >= cells_width or y >= cells_height or x < 0 or y < 0) {
return .none;
}
return self.map[y][x];
}
fn set(self: *Wave, x: usize, y: usize, cell: Cell) void {
if (x < cells_width and y < cells_height and x >= 0 and y >= 0) {
self.map[y][x] = cell;
}
}
};
const TextureKind = enum {
cpus_vs_bugs,
power_button,
socket,
lane,
ai,
cpu,
ram,
bug_null,
bug_while,
bug_stackoverflow,
transistor,
cpu_pins,
upgrade_popup,
button,
};
const SoundKind = enum {
bug_death,
bug_attack,
ram_destroyed,
cpu_place,
cpu_upgrade,
button_click,
};
const Game = struct {
global_arena: std.heap.ArenaAllocator,
frame_arena: std.heap.ArenaAllocator,
texture_map: std.AutoHashMap(TextureKind, rl.Texture2D),
sound_map: std.AutoHashMap(SoundKind, rl.Sound),
font_title: rl.Font,
font_normal: rl.Font,
screen_state: union(enum) {
main: ScreenMainMenu,
battle: ScreenBattle,
gameover: ScreenGameOver,
victory: ScreenVictory,
},
fn init() Game {
var global_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
const ga = global_arena.allocator();
var texture_map = std.AutoHashMap(TextureKind, rl.Texture2D).init(ga);
var sound_map = std.AutoHashMap(SoundKind, rl.Sound).init(ga);
// TODO: make this texture loading more dynamic - DO THIS AFTER JAM
const cpus_vs_bugs = rl.loadTexture("assets/img/cpus-vs-bugs.png") catch unreachable;
const power_button = rl.loadTexture("assets/img/power-button.png") catch unreachable;
const lane = rl.loadTexture("assets/img/lane.png") catch unreachable;
const socket = rl.loadTexture("assets/img/socket.png") catch unreachable;
const ai = rl.loadTexture("assets/img/ai.png") catch unreachable;
const bug_so = rl.loadTexture("assets/img/stackoverflow.png") catch unreachable;
const bug_null = rl.loadTexture("assets/img/nullptr-deref.png") catch unreachable;
const bug_while = rl.loadTexture("assets/img/while1.png") catch unreachable;
const ram = rl.loadTexture("assets/img/ram.png") catch unreachable;
const transistor = rl.loadTexture("assets/img/transistor.png") catch unreachable;
const cpu_pins = rl.loadTexture("assets/img/cpu_pins.png") catch unreachable;
const cpu = rl.loadTexture("assets/img/cpu.png") catch unreachable;
const upgrade_popup = rl.loadTexture("assets/img/upgrade-popup.png") catch unreachable;
const button = rl.loadTexture("assets/img/button.png") catch unreachable;
texture_map.put(.cpus_vs_bugs, cpus_vs_bugs) catch unreachable;
texture_map.put(.power_button, power_button) catch unreachable;
texture_map.put(.socket, socket) catch unreachable;
texture_map.put(.lane, lane) catch unreachable;
texture_map.put(.ai, ai) catch unreachable;
texture_map.put(.bug_null, bug_null) catch unreachable;
texture_map.put(.bug_stackoverflow, bug_so) catch unreachable;
texture_map.put(.bug_while, bug_while) catch unreachable;
texture_map.put(.ram, ram) catch unreachable;
texture_map.put(.transistor, transistor) catch unreachable;
texture_map.put(.cpu_pins, cpu_pins) catch unreachable;
texture_map.put(.cpu, cpu) catch unreachable;
texture_map.put(.upgrade_popup, upgrade_popup) catch unreachable;
texture_map.put(.button, button) catch unreachable;
const bug_death = rl.loadSound("assets/sfx/bug-death.wav") catch unreachable;
rl.setSoundVolume(bug_death, 0.3);
const bug_attack = rl.loadSound("assets/sfx/bug-attack.wav") catch unreachable;
rl.setSoundVolume(bug_attack, 0.5);
const ram_destroyed = rl.loadSound("assets/sfx/ram-destroyed.wav") catch unreachable;
rl.setSoundVolume(ram_destroyed, 0.8);
const cpu_place = rl.loadSound("assets/sfx/cpu-place.mp3") catch unreachable;
rl.setSoundVolume(cpu_place, 0.8);
const cpu_upgrade = rl.loadSound("assets/sfx/cpu-upgrade.wav") catch unreachable;
rl.setSoundVolume(cpu_upgrade, 0.8);
const button_click = rl.loadSound("assets/sfx/button-click.mp3") catch unreachable;
rl.setSoundVolume(button_click, 0.5);
sound_map.put(.bug_death, bug_death) catch unreachable;
sound_map.put(.bug_attack, bug_attack) catch unreachable;
sound_map.put(.ram_destroyed, ram_destroyed) catch unreachable;
sound_map.put(.cpu_place, cpu_place) catch unreachable;
sound_map.put(.cpu_upgrade, cpu_upgrade) catch unreachable;
sound_map.put(.button_click, button_click) catch unreachable;
const font_title = rl.loadFontEx("assets/font/DepartureMonoNerdFontMono-Regular.otf", 80, null) catch unreachable;
const font_normal = rl.loadFontEx("assets/font/GohuFont14NerdFontMono-Regular.ttf", 80, null) catch unreachable;
return .{
.global_arena = global_arena,
.frame_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator),
.texture_map = texture_map,
.sound_map = sound_map,
.font_title = font_title,
.font_normal = font_normal,
.screen_state = .{
.main = .init(), // MARK
// .gameover = .init(),
// .victory = .init(30.53288888, false, 0.5732),
// .battle = .init(),
},
};
}
fn deinit(self: *Game) void {
self.global_arena.deinit();
self.frame_arena.deinit();
}
fn frameStart(self: *Game) void {
const reset_successful = self.frame_arena.reset(.retain_capacity);
assert(reset_successful);
}
fn process(self: *Game) void {
const dt = rl.getFrameTime();
// std.debug.print("Delta time: {d}\n", .{dt});
// TODO: find a better method of mutating the original
switch (self.screen_state) {
.main => {
self.screen_state.main.update(self, dt);
},
.battle => {
self.screen_state.battle.update(self, dt);
},
.gameover => {
self.screen_state.gameover.update(self, dt);
},
.victory => {
self.screen_state.victory.update(self, dt);
},
}
}
fn render(self: *Game) void {
rl.beginDrawing();
defer rl.endDrawing();
// TODO: maybe add a system for each screen to be able to give a color to set the background?
// just so the render method of the screens doesn't have to directly call rl.clearBackground
rl.clearBackground(bg_color);
// TODO: find a better method of mutating the original
switch (self.screen_state) {
.main => {
self.screen_state.main.render(self);
},
.battle => {
self.screen_state.battle.render(self);
},
.gameover => {
self.screen_state.gameover.render(self);
},
.victory => {
self.screen_state.victory.render(self);
},
}
}
};
const ScreenMainMenu = struct {
camera: rl.Camera2D,
pressed_start: bool = false,
const start_button_scaling = 5;
const start_button_size: f32 = @floatFromInt(cell_size * start_button_scaling);
const title_size: f32 = @floatFromInt(cell_size * 5);
fn init() ScreenMainMenu {
return .{
.camera = .{
.target = .{ .x = 128, .y = 128 },
.offset = .{
.x = @as(f32, @floatFromInt(screen_width)) / 2,
.y = @as(f32, @floatFromInt(screen_height)) / 2,
},
.rotation = 0,
.zoom = @as(f32, @floatFromInt(screen_height)) / world_height,
},
};
}
fn update(self: *ScreenMainMenu, game: *Game, dt: f32) void {
_ = dt;
self.updateCamera();
if (self.pressed_start) {
game.screen_state = .{ .battle = .init() };
}
}
fn render(self: *ScreenMainMenu, game: *Game) void {
rl.beginMode2D(self.camera);
defer rl.endMode2D();
const scaling_factor = 2;
const title_pos: rl.Vector2 = .{
.x = (world_width - (ScreenMainMenu.title_size * scaling_factor)) / 2,
.y = cell_size / 2, // padding
};
rl.drawTexturePro(
game.texture_map.get(.cpus_vs_bugs).?,
.{
.x = 0,
.y = 0,
.width = ScreenMainMenu.title_size,
.height = cell_size,
},
.{
.x = title_pos.x,
.y = title_pos.y,
.width = ScreenMainMenu.title_size * scaling_factor,
.height = cell_size * scaling_factor,
},
rl.Vector2.zero(),
0,
rl.Color.white,
);
const padding = cell_size + cell_size / 2;
self.pressed_start = textureButtonScaled(
game,
&self.camera,
game.texture_map.get(.power_button).?,
rl.Vector2.init(
(world_width - ScreenMainMenu.start_button_size) / 2,
(world_height - ScreenMainMenu.start_button_size) / 2 + padding,
),
0,
ScreenMainMenu.start_button_scaling,
);
}
fn updateCamera(self: *ScreenMainMenu) void {
screen_width = rl.getScreenWidth();
screen_height = rl.getScreenHeight();
self.camera.offset = .{
.x = @as(f32, @floatFromInt(screen_width)) / 2,
.y = @as(f32, @floatFromInt(screen_height)) / 2,
};
self.camera.target = .{
.x = world_width / 2.0,
.y = world_height / 2.0,
};
// Take the average between the ratios
// This avoids "cheating" by changing the ratio to an extreme value
// in order to see more terrain in a certain axis
const width_ratio = @as(f32, @floatFromInt(screen_width)) / world_width;
const height_ratio = @as(f32, @floatFromInt(screen_height)) / world_height;
self.camera.zoom = (width_ratio + height_ratio) / 2;
}
};
const ScreenGameOver = struct {
camera: rl.Camera2D,
pressed_retry: bool = false,
pressed_menu: bool = false,
dt: f32 = -1.0,
game_over_text: [:0]const u8 = "Game Over",
game_over_size: f32 = 22,
game_over_spacing: f32 = 1.0,
game_over_color: rl.Color = rl.Color.red,
game_over_measurement: rl.Vector2 = .{ .x = 0, .y = 0 },
game_over_widths: [9]f32 = undefined,
game_over_height: f32 = 0,
game_over_speed: f32 = 2.0,
game_over_animation_offset: f32 = 0.2,
game_over_animation_size: f32 = 0.3,
initiated: bool = false,
const button_menu_width: f32 = @floatFromInt(cell_size * 7);
const button_menu_height: f32 = @floatFromInt(cell_size);
fn init() ScreenGameOver {
return .{
.camera = .{
.target = .{ .x = 128, .y = 128 },
.offset = .{
.x = @as(f32, @floatFromInt(screen_width)) / 2,
.y = @as(f32, @floatFromInt(screen_height)) / 2,
},
.rotation = 0,
.zoom = @as(f32, @floatFromInt(screen_height)) / world_height,
},
};
}
fn update(self: *ScreenGameOver, game: *Game, dt: f32) void {
if (self.initiated) {
self.dt += dt;
}
self.updateCamera();
// Must be the last thing
if (self.pressed_retry) {
game.screen_state = .{ .battle = .init() };
} else if (self.pressed_menu) {
game.screen_state = .{ .main = .init() };
}
}
fn updateCamera(self: *ScreenGameOver) void {
screen_width = rl.getScreenWidth();
screen_height = rl.getScreenHeight();
self.camera.offset = .{
.x = @as(f32, @floatFromInt(screen_width)) / 2,
.y = @as(f32, @floatFromInt(screen_height)) / 2,
};
self.camera.target = .{
.x = world_width / 2.0,
.y = world_height / 2.0,
};
// Take the average between the ratios
// This avoids "cheating" by changing the ratio to an extreme value
// in order to see more terrain in a certain axis
const width_ratio = @as(f32, @floatFromInt(screen_width)) / world_width;
const height_ratio = @as(f32, @floatFromInt(screen_height)) / world_height;
self.camera.zoom = (width_ratio + height_ratio) / 2;
}
fn render(self: *ScreenGameOver, game: *Game) void {
rl.beginMode2D(self.camera);
defer rl.endMode2D();
if (!self.initiated) {
var i: usize = 0;
const len: usize = self.game_over_text.len;
if (len != self.game_over_widths.len) {
std.log.err("Game over text length isn't covered properly. adjust ScreenGameOver.game_over_widths and ScreenGameOver.game_over_characters accordingly to {} instead of {}", .{ len, self.game_over_widths.len });
return;
}
while (i < len) : (i += 1) {
const substring = self.game_over_text[i..];
const measurement = rl.measureTextEx(game.font_title, substring, self.game_over_size, self.game_over_spacing);
if (i == 0) {
self.game_over_height = measurement.y;
}
self.game_over_widths[i] = measurement.x;
}
self.initiated = true;
}
// total width and height for the game over text
const tx = self.game_over_widths[0];
const ty = self.game_over_height;
// we have widths for the title with the start chopped off.
// let's calculate the right side
const rx = (world_width + tx) / 2;
const ry = (world_height - ty) / 2;
var i: usize = 0;
const len: usize = self.game_over_text.len;
while (i < len) {
const fi: f32 = @as(f32, @floatFromInt(i));
const width = self.game_over_widths[i];
const x = rx - width;
// feel free to find a better way to extract single characters:
const char = rl.textFormat("%c", .{self.game_over_text[i]});
const y = ry - ty * self.game_over_animation_size * sin((self.dt + self.game_over_animation_offset * fi) * self.game_over_speed);
rl.drawTextEx(game.font_title, char, .{ .x = x, .y = y }, self.game_over_size, self.game_over_spacing, self.game_over_color);
i += 1;
}
self.pressed_menu = labelButton(
game,
&self.camera,
"Back to main menu",
rl.Rectangle.init(
(world_width - ScreenGameOver.button_menu_width) / 2,
(world_height - ScreenGameOver.button_menu_height) / 2 + cell_size * 1.25,
ScreenGameOver.button_menu_width,
ScreenGameOver.button_menu_height,
),
.{
.font = game.font_title,
.font_size = 10,
.spacing = 5,
},
);
self.pressed_retry = labelButton(
game,
&self.camera,
"Retry",
rl.Rectangle.init(
(world_width - ScreenGameOver.button_menu_width) / 2,
(world_height - ScreenGameOver.button_menu_height) / 2 + cell_size * 2.25,
ScreenGameOver.button_menu_width,
ScreenGameOver.button_menu_height,
),
.{
.font = game.font_title,
.font_size = 10,
.spacing = 5,
},
);
}
};
const ScreenVictory = struct {
camera: rl.Camera2D,
pressed_play_again: bool = false,
pressed_menu: bool = false,
dt: f32 = -1.0,
text: [:0]const u8 = "You win!",
size: f32 = 22,
spacing: f32 = 1.0,
color: rl.Color = rl.Color.green,
measurement: rl.Vector2 = .{ .x = 0, .y = 0 },
widths: [8]f32 = undefined,
height: f32 = 0,
speed: f32 = 2.0,
animation_offset: f32 = 0.2,
animation_size: f32 = 0.3,
initiated: bool = false,
completion_time: f32,
skipped: bool,
ram_percentage: f32,
const button_menu_width: f32 = @floatFromInt(cell_size * 7);
const button_menu_height: f32 = @floatFromInt(cell_size);
fn init(completion_time: f32, skipped: bool, ram_percentage: f32) ScreenVictory {
return .{
.camera = .{
.target = .{ .x = 128, .y = 128 },
.offset = .{
.x = @as(f32, @floatFromInt(screen_width)) / 2,
.y = @as(f32, @floatFromInt(screen_height)) / 2,
},
.rotation = 0,
.zoom = @as(f32, @floatFromInt(screen_height)) / world_height,
},
.completion_time = completion_time,
.skipped = skipped,
.ram_percentage = ram_percentage,
};
}
fn update(self: *ScreenVictory, game: *Game, dt: f32) void {
if (self.initiated) {
self.dt += dt;
}
self.updateCamera();
// Must be the last thing
if (self.pressed_play_again) {
game.screen_state = .{ .battle = .init() };
} else if (self.pressed_menu) {
game.screen_state = .{ .main = .init() };
}
}
fn updateCamera(self: *ScreenVictory) void {
screen_width = rl.getScreenWidth();
screen_height = rl.getScreenHeight();
self.camera.offset = .{
.x = @as(f32, @floatFromInt(screen_width)) / 2,
.y = @as(f32, @floatFromInt(screen_height)) / 2,
};
self.camera.target = .{
.x = world_width / 2.0,
.y = world_height / 2.0,
};
// Take the average between the ratios
// This avoids "cheating" by changing the ratio to an extreme value
// in order to see more terrain in a certain axis
const width_ratio = @as(f32, @floatFromInt(screen_width)) / world_width;
const height_ratio = @as(f32, @floatFromInt(screen_height)) / world_height;
self.camera.zoom = (width_ratio + height_ratio) / 2;
}
fn render(self: *ScreenVictory, game: *Game) void {
rl.beginMode2D(self.camera);
defer rl.endMode2D();
if (!self.initiated) {
var i: usize = 0;
const len: usize = self.text.len;
assert(len == self.widths.len);
while (i < len) : (i += 1) {
const substring = self.text[i..];
const measurement = rl.measureTextEx(game.font_title, substring, self.size, self.spacing);
if (i == 0) {
self.height = measurement.y;
}
self.widths[i] = measurement.x;
}
self.initiated = true;
}
// total width and height for the victory text
const tx = self.widths[0];
const ty = self.height;
// we have widths for the title with the start chopped off.
// let's calculate the right side
const rx = (world_width + tx) / 2;
const ry = (world_height - ty) / 2 - (cell_size * 2);
var i: usize = 0;
const len: usize = self.text.len;
while (i < len) {
const fi: f32 = @as(f32, @floatFromInt(i));
const width = self.widths[i];
const x = rx - width;
// feel free to find a better way to extract single characters:
const char = rl.textFormat("%c", .{self.text[i]});
const y = ry - ty * self.animation_size * sin((self.dt + self.animation_offset * fi) * self.speed);
rl.drawTextEx(game.font_title, char, .{ .x = x, .y = y }, self.size, self.spacing, self.color);
i += 1;
}
const skipped_text = if (self.skipped) "Skipped: true" else "Skipped: false";
const skipped_dim = rl.measureTextEx(game.font_title, skipped_text, self.size, self.spacing);
rl.drawTextEx(
game.font_title,
skipped_text,
.{
.x = (world_width - skipped_dim.x) / 2,
.y = (world_height - skipped_dim.y) / 2 + (cell_size * 3.5),
},
self.size,
self.spacing,
.white,
);
const completion_time_text = std.fmt.allocPrintZ(
game.frame_arena.allocator(),
"Time: {d:.2}s",
.{self.completion_time},
) catch unreachable;
const completion_time_dim = rl.measureTextEx(game.font_title, completion_time_text, self.size, self.spacing);
rl.drawTextEx(
game.font_title,
completion_time_text,
.{
.x = (world_width - completion_time_dim.x) / 2,
.y = (world_height - completion_time_dim.y) / 2 - (cell_size / 2) - (cell_size / 4),
},
self.size,
self.spacing,
.white,
);
const ram_percentage_text = std.fmt.allocPrintZ(
game.frame_arena.allocator(),
"RAM: {d}%",
.{std.math.round(self.ram_percentage * 100)},
) catch unreachable;
const ram_percentage_dim = rl.measureTextEx(game.font_title, ram_percentage_text, self.size, self.spacing);
rl.drawTextEx(
game.font_title,
ram_percentage_text,
.{
.x = (world_width - ram_percentage_dim.x) / 2,
.y = (world_height - ram_percentage_dim.y) / 2 + (cell_size / 4),
},
self.size,
self.spacing,
.white,
);
self.pressed_menu = labelButton(
game,
&self.camera,
"Back to main menu",
rl.Rectangle.init(
(world_width - ScreenVictory.button_menu_width) / 2,
(world_height - ScreenVictory.button_menu_height) / 2 + cell_size * 1.25,
ScreenVictory.button_menu_width,
ScreenVictory.button_menu_height,
),
.{
.font = game.font_title,
.font_size = 10,
.spacing = 5,
},
);
self.pressed_play_again = labelButton(
game,
&self.camera,
"Play again",
rl.Rectangle.init(
(world_width - ScreenVictory.button_menu_width) / 2,
(world_height - ScreenVictory.button_menu_height) / 2 + cell_size * 2.25,
ScreenVictory.button_menu_width,
ScreenVictory.button_menu_height,
),
.{
.font = game.font_normal,
.font_size = 10,
.spacing = 5,
},
);
}
};
const ScreenBattle = struct {
camera: rl.Camera2D,
wave: Wave,
wave_number: u8,
ram: f32, // health of the player
transistors: u32 = initial_cpu_transistor_cost,
cpu_transistor_cost: u32 = initial_cpu_transistor_cost,
wave_over_timer: f32 = 0,
skip: bool = false,
skipped: bool = false,
completion_time: f32 = 0,
popup: bool = false,
konami_counter: u8 = 0,
const max_ram = 100;
const initial_cpu_transistor_cost = 50;
const wave_continue_delay = 2; // seconds
fn init() ScreenBattle {
const wave_number: u8 = 1; // MARK
return .{
.wave_number = wave_number,
.wave = .init(wave_number),
.camera = .{
.target = .{ .x = 128, .y = 128 },
.offset = .{
.x = @as(f32, @floatFromInt(screen_width)) / 2,
.y = @as(f32, @floatFromInt(screen_height)) / 2,
},
.rotation = 0,
.zoom = @as(f32, @floatFromInt(screen_height)) / world_height,
},
.ram = max_ram,
};
}
fn deinit(self: *ScreenBattle) void {
self.wave.deinit();
}
fn update(self: *ScreenBattle, game: *Game, dt: f32) void {
self.completion_time += dt;
self.handlePlayerInput(game, dt);
self.updateCpuAndBugs(game, dt);
const wave_over = self.wave.update(dt, &self.ram, game);
if (wave_over) {
self.wave_over_timer += dt;
}
self.updateCamera();
if (self.ram <= 0) {
self.deinit();
game.screen_state = .{ .gameover = .init() };
} else if (self.wave_over_timer >= wave_continue_delay or self.skip) {
self.skip = false;
self.wave_over_timer = 0;
self.wave_number += 1;
self.cpu_transistor_cost = initial_cpu_transistor_cost;
self.transistors = self.cpu_transistor_cost;
if (self.wave_number > 3) {
self.deinit();
game.screen_state = .{ .victory = .init(self.completion_time, self.skipped, self.ram / max_ram) };
} else {
self.wave.deinit();
self.wave = .init(self.wave_number);
}
}
}
fn handlePlayerInput(self: *ScreenBattle, game: *Game, dt: f32) void {
_ = dt;
const msp = getMousePos(&self.camera);
const mx: usize = @intFromFloat(msp.x / cell_size);
const my: usize = @intFromFloat(msp.y / cell_size);
if (rl.isMouseButtonPressed(rl.MouseButton.left)) {
if (self.wave.get(mx, my) == .lane and edit) {
self.wave.set(mx, my, .socket);
} else if (self.wave.get(mx, my) == .socket and edit) {
self.wave.set(mx, my, .lane);
} else if (self.wave.get(mx, my) == .none and edit) {
self.wave.set(mx, my, .socket);
}
if (self.wave.get(mx, my) == .socket and self.transistors >= self.cpu_transistor_cost) {
self.wave.set(mx, my, .{ .cpu = .{} });
self.transistors -= self.cpu_transistor_cost;
self.cpu_transistor_cost *= 2;
rl.playSound(game.sound_map.get(.cpu_place).?);
} else if (self.wave.get(mx, my) == .cpu) {
var cpu = &self.wave.at(mx, my).?.cpu;
if (cpu.upgradeBusWidth()) {
rl.playSound(game.sound_map.get(.cpu_upgrade).?);
}
}
} else if (rl.isMouseButtonPressed(rl.MouseButton.right)) {
if (self.wave.get(mx, my) == .cpu) {
var cpu = &self.wave.at(mx, my).?.cpu;
if (cpu.upgradeCacheSize()) {
rl.playSound(game.sound_map.get(.cpu_upgrade).?);
}
}
if (edit) {
self.wave.set(mx, my, .none);
}
}
if (rl.isKeyPressed(rl.KeyboardKey.s) and debug) {
const f = std.io.getStdErr().writer();
std.zon.stringify.serializeArbitraryDepth(self.wave.map, .{
.whitespace = false,
}, f) catch unreachable;
f.writeByte('\n') catch unreachable;
unreachable;
} else if (rl.isKeyPressed(rl.KeyboardKey.s)) {
self.skip = true;
self.skipped = true;
}
var mut_konami_code = konami_code;
if (readCode(&self.konami_counter, &mut_konami_code)) {
self.transistors = std.math.maxInt(i32);
}
}
fn updateCpuAndBugs(self: *ScreenBattle, game: *Game, dt: f32) void {
const map = self.wave.map;
for (0..map.len) |y| {
for (0..map[0].len) |x| {
const cell = self.wave.at(x, y).?;
switch (cell.*) {
.cpu => {
var cpu = &cell.cpu;
const cpu_center = rl.Vector2.init(
@as(f32, @floatFromInt(x)) * cell_size + cell_size / 2,
@as(f32, @floatFromInt(y)) * cell_size + cell_size / 2,
);
var count: u32 = 0;
const radius = cpu.cache_size * cell_size;
for (self.wave.bugs.items) |*bug| {
if (bug.dead) {
continue;
}
if (count >= cpu.cores()) {
break;
}
const distance = cpu_center.subtract(bug.position).length();
if (distance > radius) {
continue;
}
if (bug.damage(cpu.damage() * dt)) {
cpu.debugs += 1;
rl.playSound(game.sound_map.get(.bug_death).?);
self.transistors += bug.transistors();
}
count += 1;
}
},
else => continue,
}
}
}
}
fn updateCamera(self: *ScreenBattle) void {
screen_width = rl.getScreenWidth();
screen_height = rl.getScreenHeight();
self.camera.offset = .{
.x = @as(f32, @floatFromInt(screen_width)) / 2,
.y = @as(f32, @floatFromInt(screen_height)) / 2,
};
self.camera.target = .{
.x = world_width / 2.0,
.y = world_height / 2.0,
};
// Take the average between the ratios
// This avoids "cheating" by changing the ratio to an extreme value
// in order to see more terrain in a certain axis
const width_ratio = @as(f32, @floatFromInt(screen_width)) / world_width;
const height_ratio = @as(f32, @floatFromInt(screen_height)) / world_height;
self.camera.zoom = (width_ratio + height_ratio) / 2;
}
fn render(self: *ScreenBattle, game: *Game) void {
const a = game.frame_arena.allocator();
const map = self.wave.map;
self.popup = false; // reset popup
{
rl.beginMode2D(self.camera);
defer rl.endMode2D();
// TODO: make bugs render between (on top of) lanes and (under) AI
for (0..map.len) |y| {
for (0..map[0].len) |x| {
self.drawCell(game, x, y);
}
}
for (0..map.len) |y| {
for (0..map[0].len) |x| {
const cell = self.wave.get(x, y);
switch (cell) {
.cpu => |cpu| {
const cpu_center = rl.Vector2.init(
@as(f32, @floatFromInt(x)) * cell_size + cell_size / 2,
@as(f32, @floatFromInt(y)) * cell_size + cell_size / 2,
);
var count: u32 = 0;
const radius = cpu.cache_size * cell_size;
for (self.wave.bugs.items) |bug| {
if (bug.dead) {
continue;
}
if (count >= cpu.cores()) {
break;
}
const distance = cpu_center.subtract(bug.position).length();
if (distance > radius) {
continue;
}
rl.drawLineEx(cpu_center, bug.position, cpu.bus_width, rl.Color.red);
count += 1;
}
if (debug) {
rl.drawCircleLinesV(cpu_center, cpu.cache_size * cell_size, rl.Color.green);
}
},
else => continue,
}
}
}
for (self.wave.bugs.items) |bug| {
if (bug.dead) {
continue;
}
const texture = switch (bug.kind) {
.nullptr_deref => game.texture_map.get(.bug_null).?,
.stack_overflow => game.texture_map.get(.bug_stackoverflow).?,
.infinite_loop => game.texture_map.get(.bug_while).?,
};
// const angle_deg = bug.angle;
const angle_deg = 0;
const angle_rad = angle_deg * (std.math.pi / 180.0);
const origin_x = cos(angle_rad) * (cell_size / 2) - sin(angle_rad) * (cell_size / 2);
const origin_y = sin(angle_rad) * (cell_size / 2) + cos(angle_rad) * (cell_size / 2);
const draw_x = bug.position.x - origin_x;
const draw_y = bug.position.y - origin_y;
rl.drawTexturePro(
texture,
.{ .x = bug.animation_state * cell_size, .y = 0, .width = cell_size, .height = cell_size },
.{ .x = draw_x, .y = draw_y, .width = cell_size, .height = cell_size },
rl.Vector2.zero(),
angle_deg,
rl.Color.white,
);
}
const font_size = 24;
const transistor_texture = game.texture_map.get(.transistor).?;
rl.drawTexture(transistor_texture, 0, cell_size / 4, rl.Color.white);
const transistors_count = std.fmt.allocPrintZ(a, "{d}", .{self.transistors}) catch unreachable;
rl.drawTextEx(
game.font_title,
transistors_count,
.{ .x = cell_size, .y = (cell_size + font_size) / 4 },
font_size,
1,
.white,
);
const offset = world_height - cell_size;
const cpu_texture = game.texture_map.get(.cpu_pins).?;
rl.drawTexture(cpu_texture, 0, offset, rl.Color.white);
rl.drawTextureEx(
transistor_texture,
.{ .x = cell_size + cell_size / 3, .y = offset + 6 },
0,
0.5,
rl.Color.white,
);
const cpu_cost = std.fmt.allocPrintZ(a, "= {d}", .{self.cpu_transistor_cost}) catch unreachable;
rl.drawTextEx(
game.font_title,
cpu_cost,
.{ .x = cell_size, .y = offset + (font_size) / 8 },
font_size,
1,
.white,
);
if (self.popup) {
const msp = getMousePos(&self.camera);
const upgrade_texture = game.texture_map.get(.upgrade_popup).?;
const scale = 2;
const pos = msp.subtract(.{ .x = cell_size / 2 * scale, .y = cell_size * scale });
rl.drawTexturePro(
upgrade_texture,
.{ .x = 0, .y = 0, .width = cell_size, .height = cell_size },
.{ .x = pos.x, .y = pos.y, .width = cell_size * scale, .height = cell_size * scale },
rl.Vector2.zero(),
0,
rl.Color.white,
);
}
}
if (debug) {
rl.drawRectangle(
10,
10,
@divTrunc(screen_width, 5),
@divTrunc(screen_height, 5),
rl.fade(rl.Color.white, 0.6),
);
rl.drawRectangleLines(
10,
10,
@divTrunc(screen_width, 5),
@divTrunc(screen_height, 5),
rl.Color.black,
);
const debug_font_size = @divTrunc(screen_width, 80);
const debug_info = std.fmt.allocPrintZ(a,
\\FPS: {}
\\Screen: {}x{}
\\World: {}x{} ({})
\\Bugs: {}
\\Ram: {d}/{d}
\\Transistors: {d}
, .{
rl.getFPS(),
screen_width,
screen_height,
world_width,
world_height,
cell_size,
self.wave.bugs.items.len,
self.ram,
max_ram,
self.transistors,
}) catch return;
rl.drawText(debug_info, 20, 20, debug_font_size, .black);
}
}
fn drawCell(self: *ScreenBattle, game: *Game, x: usize, y: usize) void {
switch (self.wave.map[y][x]) {
.none => return,
.socket => {
const texture = game.texture_map.get(.socket).?;
rl.drawTexture(texture, @intCast(x * cell_size), @intCast(y * cell_size), rl.Color.white);
},
.lane => {
const texture = game.texture_map.get(.lane).?;
const lane_left = self.wave.get(x - 1, y).isLaneConnected();
const lane_right = self.wave.get(x + 1, y).isLaneConnected();
const lane_top = self.wave.get(x, y - 1).isLaneConnected();
const lane_bottom = self.wave.get(x, y + 1).isLaneConnected();
var offset: f32 = undefined;
// Choose the correct sprite
if (lane_top and lane_left) {
offset = 3;
} else if (lane_top and lane_right) {
offset = 2;
} else if (lane_bottom and lane_left) {
offset = 5;
} else if (lane_bottom and lane_right) {
offset = 4;
} else if (lane_left or lane_right) {
offset = 0;
} else if (lane_top or lane_bottom) {
offset = 1;
}
rl.drawTextureRec(
texture,
.{ .x = offset * cell_size, .y = 0, .width = cell_size, .height = cell_size },
.{ .x = @floatFromInt(x * cell_size), .y = @floatFromInt(y * cell_size) },
rl.Color.white,
);
},
.cpu => |cpu| {
const texture = game.texture_map.get(.cpu).?;
const msp = getMousePos(&self.camera);
const mx: usize = @intFromFloat(msp.x / cell_size);
const my: usize = @intFromFloat(msp.y / cell_size);
const hovered = mx == x and my == y;
if (hovered) {
rl.drawRectangle(
@intCast(x * cell_size),
@intCast(y * cell_size),
cell_size,
cell_size,
highlight_color,
);
}
const offset: f32 = @floatFromInt(cpu.level() - 1);
rl.drawTextureRec(
texture,
.{ .x = offset * cell_size, .y = 0, .width = cell_size, .height = cell_size },
.{ .x = @floatFromInt(x * cell_size), .y = @floatFromInt(y * cell_size) },
rl.Color.white,
);
if (cpu.hasUpgrades() and hovered) {
self.popup = true;
}
},
.ai => {
const texture = game.texture_map.get(.ai).?;
rl.drawTexture(texture, @intCast(x * cell_size), @intCast(y * cell_size), rl.Color.white);
},
.ram => {
const texture = game.texture_map.get(.ram).?;
const offset: f32 = if (self.ram == 0) 4 else if (self.ram / max_ram > 0.75) 0 else if (self.ram / max_ram > 0.5) 1 else if (self.ram / max_ram > 0.25) 2 else 3;
rl.drawTextureRec(
texture,
.{ .x = offset * cell_size, .y = 0, .width = cell_size, .height = cell_size * 3 },
.{ .x = @floatFromInt(x * cell_size), .y = @floatFromInt(y * cell_size - cell_size) },
rl.Color.white,
);
},
}
}
};