const std = @import("std"); const build_options = @import("build_options"); const log = std.log.scoped(.engine); const InputSystem = @import("./input.zig"); const KeyCode = InputSystem.KeyCode; const MouseButton = InputSystem.MouseButton; const AudioSystem = @import("./audio/root.zig"); const AudioData = AudioSystem.Data; const AudioCommand = AudioSystem.Command; const GraphicsSystem = @import("./graphics.zig"); const TextureId = GraphicsSystem.TextureId; const GraphicsCommand = GraphicsSystem.Command; const Font = GraphicsSystem.Font; const Sprite = GraphicsSystem.Sprite; const Math = @import("./math.zig"); const Rect = Math.Rect; const Vec4 = Math.Vec4; const Vec2 = Math.Vec2; const rgb = Math.rgb; pub const Nanoseconds = u64; const Frame = @This(); pub const Input = struct { keyboard: InputSystem.ButtonStateSet(KeyCode), mouse_button: InputSystem.ButtonStateSet(MouseButton), mouse_position: ?Vec2, mouse_delta: Vec2, pub const empty = Input{ .keyboard = .empty, .mouse_button = .empty, .mouse_position = null, .mouse_delta = .zero, }; }; pub const KeyState = struct { down: bool, pressed: bool, released: bool, down_duration: ?f64, pub const RepeatOptions = struct { first_at: f64 = 0, period: f64 }; pub fn repeat(self: KeyState, last_repeat_at: *?f64, opts: RepeatOptions) bool { if (!self.down) { last_repeat_at.* = null; return false; } const down_duration = self.down_duration.?; if (last_repeat_at.* != null) { if (down_duration >= last_repeat_at.*.? + opts.period) { last_repeat_at.* = last_repeat_at.*.? + opts.period; return true; } } else { if (down_duration >= opts.first_at) { last_repeat_at.* = opts.first_at; return true; } } return false; } }; pub const Audio = struct { commands: std.ArrayList(AudioCommand), pub const empty = Audio{ .commands = .empty }; }; pub const Graphics = struct { clear_color: Vec4, screen_size: Vec2, canvas_size: ?Vec2, scissor_stack: std.ArrayList(Rect), commands: std.ArrayList(GraphicsCommand), pub const empty = Graphics{ .clear_color = rgb(0, 0, 0), .screen_size = .init(0, 0), .canvas_size = null, .scissor_stack = .empty, .commands = .empty }; }; arena: std.heap.ArenaAllocator, time_ns: Nanoseconds, dt_ns: Nanoseconds, input: Input, audio: Audio, graphics: Graphics, show_debug: bool, hide_cursor: bool, pub fn init(self: *Frame, gpa: std.mem.Allocator) void { self.* = Frame{ .arena = std.heap.ArenaAllocator.init(gpa), .time_ns = 0, .dt_ns = 0, .input = .empty, .audio = .empty, .graphics = .empty, .show_debug = false, .hide_cursor = false }; } pub fn deinit(self: *Frame) void { self.arena.deinit(); } pub fn deltaTime(self: Frame) f32 { return @as(f32, @floatFromInt(self.dt_ns)) / std.time.ns_per_s; } pub fn time(self: Frame) f64 { return @as(f64, @floatFromInt(self.time_ns)) / std.time.ns_per_s; } pub fn isKeyDown(self: Frame, key_code: KeyCode) bool { const keyboard = &self.input.keyboard; return keyboard.down.contains(key_code); } pub fn getKeyDownDuration(self: Frame, key_code: KeyCode) ?f32 { if (!self.isKeyDown(key_code)) { return null; } const keyboard = &self.input.keyboard; const pressed_at_ns = keyboard.pressed_at.get(key_code).?; const duration_ns = self.time_ns - pressed_at_ns; return @as(f32, @floatFromInt(duration_ns)) / std.time.ns_per_s; } pub fn isKeyPressed(self: Frame, key_code: KeyCode) bool { const keyboard = &self.input.keyboard; return keyboard.pressed.contains(key_code); } pub fn isKeyReleased(self: Frame, key_code: KeyCode) bool { const keyboard = &self.input.keyboard; return keyboard.released.contains(key_code); } pub fn getKeyState(self: Frame, key_code: KeyCode) KeyState { return KeyState{ .down = self.isKeyDown(key_code), .released = self.isKeyReleased(key_code), .pressed = self.isKeyPressed(key_code), .down_duration = self.getKeyDownDuration(key_code) }; } pub fn isMousePressed(self: Frame, button: MouseButton) bool { return self.input.mouse_button.pressed.contains(button); } pub fn isMouseDown(self: Frame, button: MouseButton) bool { return self.input.mouse_button.down.contains(button); } fn pushAudioCommand(self: *Frame, command: AudioCommand) void { const arena = self.arena.allocator(); self.audio.commands.append(arena, command) catch |e| { log.warn("Failed to play audio: {}", .{e}); }; } pub fn pushGraphicsCommand(self: *Frame, command: GraphicsCommand) void { const arena = self.arena.allocator(); self.graphics.commands.append(arena, command) catch |e|{ log.warn("Failed to push graphics command: {}", .{e}); }; } pub fn prependGraphicsCommand(self: *Frame, command: GraphicsCommand) void { const arena = self.arena.allocator(); self.graphics.commands.insert(arena, 0, command) catch |e|{ log.warn("Failed to push graphics command: {}", .{e}); }; } pub fn playAudio(self: *Frame, options: AudioCommand.Play) void { self.pushAudioCommand(.{ .play = options, }); } pub fn pushScissor(self: *Frame, rect: Rect) void { const arena = self.arena.allocator(); self.graphics.scissor_stack.append(arena, rect) catch |e| { log.warn("Failed to push scissor region: {}", .{e}); return; }; self.pushGraphicsCommand(.{ .set_scissor = rect }); } pub fn popScissor(self: *Frame) void { _ = self.graphics.scissor_stack.pop().?; const rect = self.graphics.scissor_stack.getLast(); self.pushGraphicsCommand(.{ .set_scissor = rect }); } pub fn drawCircle(self: *Frame, center: Vec2, radius: f32, color: Vec4) void { self.pushGraphicsCommand(.{ .draw_circle = .{ .center = center, .radius = radius, .color = color, } }); } pub fn drawRectangle(self: *Frame, opts: GraphicsCommand.DrawRectangle) void { self.pushGraphicsCommand(.{ .draw_rectangle = opts }); } pub fn drawLine(self: *Frame, pos1: Vec2, pos2: Vec2, color: Vec4, width: f32) void { self.pushGraphicsCommand(.{ .draw_line = .{ .pos1 = pos1, .pos2 = pos2, .width = width, .color = color } }); } pub fn drawRectanglOutline(self: *Frame, pos: Vec2, size: Vec2, color: Vec4, width: f32) void { // TODO: Don't use line segments self.drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width); self.drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width); self.drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width); self.drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width); } pub const DrawTextOptions = struct { font: Font.Id, size: f32 = 16, color: Vec4 = rgb(255, 255, 255), }; pub fn drawText(self: *Frame, position: Vec2, opts: DrawTextOptions, text: []const u8) void { const arena = self.arena.allocator(); const text_dupe = arena.dupe(u8, text) catch |e| { log.warn("Failed to draw text: {}", .{e}); return; }; self.pushGraphicsCommand(.{ .draw_text = .{ .pos = position, .text = text_dupe, .size = opts.size, .font = opts.font, .color = opts.color, } }); } pub fn drawTextFormat( self: *Frame, position: Vec2, opts: DrawTextOptions, comptime fmt: []const u8, args: anytype ) void { const arena = self.arena.allocator(); const text = std.fmt.allocPrint(arena, fmt, args) catch |e| { log.warn("Failed to draw text: {}", .{e}); return; }; self.pushGraphicsCommand(.{ .draw_text = .{ .pos = position, .text = text, .size = opts.size, .font = opts.font, .color = opts.color, } }); } pub fn pushTransform(self: *Frame, translation: Vec2, scale: Vec2) void { self.pushGraphicsCommand(.{ .push_transformation = .{ .translation = translation, .scale = scale } }); } pub fn popTransform(self: *Frame) void { self.pushGraphicsCommand(.{ .pop_transformation = {} }); }