mirror of
https://github.com/The-Memory-Managers/cpu-vs-ai.git
synced 2026-07-14 02:30:32 +00:00
1963 lines
71 KiB
Zig
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,
|
|
);
|
|
},
|
|
}
|
|
}
|
|
};
|