From 5bfa8b2c51d06b00b59773c4e48734eed07589c6 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Fri, 2 Jun 2023 12:21:32 +0200 Subject: [PATCH] Basic setup. - Allegro wrapper. - Setting up of new window etc. --- .gitignore | 4 + README.md | 15 + allegro.path | 0 allegro/allegro.zig | 697 +++++++++++++++++++++++++++ allegro/build.zig | 24 + allegro/c.zig | 8 + build.zig | 75 +++ src/assets/fonts/Cabin-OFL.txt | 93 ++++ src/assets/fonts/Cabin-Regular.ttf | Bin 0 -> 74556 bytes src/assets/images/opaque.png | Bin 0 -> 135 bytes src/assets/images/title_untitled.png | Bin 0 -> 11614 bytes src/context.zig | 92 ++++ src/engine.zig | 11 + src/engine/assets.zig | 97 ++++ src/engine/opaque_ptr.zig | 43 ++ src/engine/point.zig | 30 ++ src/engine/point_f.zig | 36 ++ src/engine/rectangle.zig | 75 +++ src/engine/rectangle_f.zig | 75 +++ src/engine/scene.zig | 90 ++++ src/engine/viewport.zig | 54 +++ src/main.zig | 98 ++++ src/main_menu_scene.zig | 36 ++ src/palette.zig | 14 + src/scene.zig | 5 + src/title_scene.zig | 45 ++ 26 files changed, 1717 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 allegro.path create mode 100644 allegro/allegro.zig create mode 100644 allegro/build.zig create mode 100644 allegro/c.zig create mode 100644 build.zig create mode 100644 src/assets/fonts/Cabin-OFL.txt create mode 100644 src/assets/fonts/Cabin-Regular.ttf create mode 100644 src/assets/images/opaque.png create mode 100644 src/assets/images/title_untitled.png create mode 100644 src/context.zig create mode 100644 src/engine.zig create mode 100644 src/engine/assets.zig create mode 100644 src/engine/opaque_ptr.zig create mode 100644 src/engine/point.zig create mode 100644 src/engine/point_f.zig create mode 100644 src/engine/rectangle.zig create mode 100644 src/engine/rectangle_f.zig create mode 100644 src/engine/scene.zig create mode 100644 src/engine/viewport.zig create mode 100644 src/main.zig create mode 100644 src/main_menu_scene.zig create mode 100644 src/palette.zig create mode 100644 src/scene.zig create mode 100644 src/title_scene.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74ee24f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vscode +zig-cache +zig-out +allegro.path diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7d45c1 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# TINS 2023 + +# Building + +For building you need: +- A recent (0.11.0-dev) Zig compiler. +- Allegro 5.28 development libraries. + +You can use `allegro.path` if your Allegro development libraries can't be natively found. In this file you can put the path to a directory contain the Allegro `lib`, `bin` and `include` folders. + +# Licenses + +## Cabin font + +[Open Font License version 1.1](src/assets/fonts/Cabin-OFL.txt) diff --git a/allegro.path b/allegro.path new file mode 100644 index 0000000..e69de29 diff --git a/allegro/allegro.zig b/allegro/allegro.zig new file mode 100644 index 0000000..63cb0f7 --- /dev/null +++ b/allegro/allegro.zig @@ -0,0 +1,697 @@ +const std = @import("std"); + +pub const c = @import("c.zig").c; + +pub const Bitmap = struct { + native: *c.ALLEGRO_BITMAP, + + pub fn destroy(self: Bitmap) void { + destroyBitmap(self); + } + + pub fn draw(self: Bitmap, x: f32, y: f32, flags: DrawFlags) void { + drawBitmap(self, x, y, flags); + } + + pub fn drawCentered(self: Bitmap, x: f32, y: f32) void { + self.draw(x - 0.5 * @intToFloat(f32, self.width()), y - 0.5 * @intToFloat(f32, self.height()), DrawFlags{}); + } + + pub fn drawCenteredScaled(self: Bitmap, x: f32, y: f32, s: f32) void { + const w = s * @intToFloat(f32, self.width()); + const h = s * @intToFloat(f32, self.height()); + self.drawScaled(x - 0.5 * w, y - 0.5 * h, w, h, DrawFlags{}); + } + + pub fn drawScaled(self: Bitmap, x: f32, y: f32, w: f32, h: f32, flags: DrawFlags) void { + drawScaledBitmap(self, 0, 0, @intToFloat(f32, self.width()), @intToFloat(f32, self.height()), x, y, w, h, flags); + } + + pub fn drawTinted(self: Bitmap, tint: Color, x: f32, y: f32, flags: DrawFlags) void { + drawTintedBitmap(self, tint, x, y, flags); + } + + pub fn drawTintedCentered(self: Bitmap, tint: Color, x: f32, y: f32) void { + self.drawTinted(tint, x - 0.5 * @intToFloat(f32, self.width()), y - 0.5 * @intToFloat(f32, self.height()), DrawFlags{}); + } + + pub fn drawTintedCenteredScaled(self: Bitmap, tint: Color, x: f32, y: f32, s: f32) void { + const w = s * @intToFloat(f32, self.width()); + const h = s * @intToFloat(f32, self.height()); + self.drawTintedScaled(tint, x - 0.5 * w, y - 0.5 * h, w, h, DrawFlags{}); + } + + pub fn drawTintedScaled(self: Bitmap, tint: Color, x: f32, y: f32, w: f32, h: f32, flags: DrawFlags) void { + drawTintedScaledBitmap(self, tint, 0, 0, @intToFloat(f32, self.width()), @intToFloat(f32, self.height()), x, y, w, h, flags); + } + + pub fn height(self: Bitmap) i32 { + return getBitmapHeight(self); + } + + pub fn width(self: Bitmap) i32 { + return getBitmapWidth(self); + } +}; + +pub const Color = struct { + native: c.ALLEGRO_COLOR, + + fn hexToU4(hex: u8) u4 { + return switch (hex) { + '0'...'9' => @intCast(u4, hex - '0'), + 'a'...'f' => @intCast(u4, hex - 'a') + 10, + 'A'...'F' => @intCast(u4, hex - 'A') + 10, + else => unreachable, + }; + } + + pub fn initFromHex(hex: []const u8) Color { + if (hex.len == 0) unreachable; + + const left: usize = if (hex[0] == '#') 1 else 0; + + if (hex.len < left + 6) unreachable; + + const r = (@intCast(u8, hexToU4(hex[left])) << 4) + @intCast(u8, hexToU4(hex[left + 1])); + const g = (@intCast(u8, hexToU4(hex[left + 2])) << 4) + @intCast(u8, hexToU4(hex[left + 3])); + const b = (@intCast(u8, hexToU4(hex[left + 4])) << 4) + @intCast(u8, hexToU4(hex[left + 5])); + if (hex.len < left + 8) { + return mapRgb(r, g, b); + } + const a = (@intCast(u8, hexToU4(hex[left + 6])) << 4) + @intCast(u8, hexToU4(hex[left + 7])); + return mapRgba(r, g, b, a); + } +}; + +pub const Display = struct { + native: *c.ALLEGRO_DISPLAY, + + pub fn destroy(self: Display) void { + destroyDisplay(self); + } + + pub fn eventSource(self: Display) EventSource { + return getDisplayEventSource(self); + } + + pub fn height(self: Display) i32 { + return getDisplayHeight(self); + } + + pub fn width(self: Display) i32 { + return getDisplayWidth(self); + } +}; + +pub const DrawFlags = packed struct(c_int) { + FLIP_HORIZONTAL: bool = false, // 0x00001 + FLIP_VERTICAL: bool = false, // 0x00002 + _NOT_USED: u30 = 0, +}; + +pub const DrawTextFlags = enum(c_int) { + NO_KERNING = -1, + ALIGN_LEFT = 0, + ALIGN_CENTER = 1, + ALIGN_RIGHT = 2, + ALIGN_LEFT_INTEGER = 4, + ALIGN_CENTER_INTEGER = 5, + ALIGN_RIGHT_INTEGER = 6, +}; + +pub const Event = c.ALLEGRO_EVENT; + +pub const EventQueue = struct { + native: *c.ALLEGRO_EVENT_QUEUE, + + pub fn destroy(self: EventQueue) void { + destroyEventQueue(self); + } + + pub fn drop(self: EventQueue) void { + dropNextEvent(self); + } + + pub fn flush(self: EventQueue) void { + flushEventQueue(self); + } + + pub fn get(self: EventQueue, event: *Event) bool { + return getNextEvent(self, event); + } + + pub fn isEmpty(self: EventQueue) bool { + return isEventQueueEmpty(self); + } + + pub fn peek(self: EventQueue, event: *Event) void { + peekNextEvent(self, event); + } + + pub fn register(self: EventQueue, source: EventSource) void { + registerEventSource(self, source); + } + + pub fn registerDisplay(self: EventQueue, display: Display) void { + self.register(display.eventSource()); + } + + pub fn registerKeyboard(self: EventQueue) void { + self.register(getKeyboardEventSource()); + } + + pub fn registerMouse(self: EventQueue) void { + self.register(getMouseEventSource()); + } + + pub fn registerTimer(self: EventQueue, timer: Timer) void { + self.register(timer); + } + + pub fn unregister(self: EventQueue, source: EventSource) void { + unregisterEventSource(self, source); + } + + pub fn unregisterDisplay(self: EventQueue, display: Display) void { + self.unregister(display.eventSource()); + } + + pub fn wait(self: EventQueue, event: *Event) void { + waitForEvent(self, event); + } +}; + +pub const EventSource = struct { + native: *c.ALLEGRO_EVENT_SOURCE, +}; + +pub const File = struct { + native: *c.ALLEGRO_FILE, + + pub fn close(file: File) void { + fclose(file); + } +}; + +pub const Font = struct { + native: *c.ALLEGRO_FONT, + + pub fn destroy(self: Font) void { + destroyFont(self); + } + + pub fn draw(self: Font, color: Color, x: f32, y: f32, flags: DrawTextFlags, text: [*:0]const u8) void { + drawText(self, color, x, y, flags, text); + } + + pub fn textWidth(self: Font, text: [*:0]const u8) i32 { + return getTextWidth(self, text); + } +}; + +pub const LoadBitmapFlags = packed struct(c_int) { + _NOT_USED_1: bool = false, + KEEP_BITMAP_FORMAT: bool = false, // 0x0002 // was a bitmap flag in 5.0 + _NOT_USED_2: u7 = 0, + NO_PREMULTIPLIED_ALPHA: bool = false, // 0x0200 // was a bitmap flag in 5.0 + _NOT_USED_3: bool = false, + KEEP_INDEX: bool = false, // 0x0800 + _NOT_USED_4: u20 = 0, +}; + +pub const LoadTtfFontFlags = packed struct(c_int) { + NO_KERNING: bool = false, + MONOCHROME: bool = false, + NO_AUTOHINT: bool = false, + _NOT_USED: u29 = 0, +}; + +pub const NewBitmapFlags = packed struct(c_int) { + MEMORY_BITMAP: bool = false, // 0x0001, + _KEEP_BITMAP_FORMAT: bool = false, // 0x0002, + FORCE_LOCKING: bool = false, // 0x0004, + NO_PRESERVE_TEXTURE: bool = false, // 0x0008, + _ALPHA_TEST: bool = false, // 0x0010, + _INTERNAL_OPENGL: bool = false, // 0x0020, + MIN_LINEAR: bool = false, // 0x0040, + MAG_LINEAR: bool = false, // 0x0080, + MIPMAP: bool = false, // 0x0100, + _NO_PREMULTIPLIED_ALPHA: bool = false, // 0x0200, + VIDEO_BITMAP: bool = false, // 0x0400, + _NOT_USED_1: bool = false, // 0x0800 + CONVERT_BITMAP: bool = false, // 0x1000, + _NOT_USED_4: u19 = 0, +}; + +pub const NewDisplayFlags = packed struct(c_int) { + WINDOWED: bool = false, + FULLSCREEN: bool = false, + OPENGL: bool = false, + DIRECT3D_INTERNAL: bool = false, + RESIZABLE: bool = false, + FRAMELESS: bool = false, + GENERATE_EXPOSE_EVENTS: bool = false, + OPENGL_3_0: bool = false, + OPENGL_FORWARD_COMPATIBLE: bool = false, + FULLSCREEN_WINDOW: bool = false, + MINIMIZED: bool = false, + PROGRAMMABLE_PIPELINE: bool = false, + GTK_TOPLEVEL_INTERNAL: bool = false, + MAXIMIZED: bool = false, + OPENGL_ES_PROFILE: bool = false, + OPENGL_CORE_PROFILE: bool = false, + _NOT_USED: u16 = 0, +}; + +pub const NewDisplayOption = enum(c_int) { + RED_SIZE, // 0 + GREEN_SIZE, // 1 + BLUE_SIZE, // 2 + ALPHA_SIZE, // 3 + RED_SHIFT, // 4 + GREEN_SHIFT, // 5 + BLUE_SHIFT, // 6 + ALPHA_SHIFT, // 7 + ACC_RED_SIZE, // 8 + ACC_GREEN_SIZE, // 9 + ACC_BLUE_SIZE, // 10 + ACC_ALPHA_SIZE, // 11 + STEREO, // 12 + AUX_BUFFERS, // 13 + COLOR_SIZE, // 14 + DEPTH_SIZE, // 15 + STENCIL_SIZE, // 16 + SAMPLE_BUFFERS, // 17 + SAMPLES, // 18 + RENDER_METHOD, // 19 + FLOAT_COLOR, // 20 + FLOAT_DEPTH, // 21 + SINGLE_BUFFER, // 22 + SWAP_METHOD, // 23 + COMPATIBLE_DISPLAY, // 24 + UPDATE_DISPLAY_REGION, // 25 + VSYNC, // 26 + MAX_BITMAP_SIZE, // 27 + SUPPORT_NPOT_BITMAP, // 28 + CAN_DRAW_INTO_BITMAP, // 29 + SUPPORT_SEPARATE_ALPHA, // 30 + AUTO_CONVERT_BITMAPS, // 31 + SUPPORTED_ORIENTATIONS, // 32 + OPENGL_MAJOR_VERSION, // 33 + OPENGL_MINOR_VERSION, // 34 + DEFAULT_SHADER_PLATFORM, // 35 +}; + +pub const OptionImportance = enum(c_int) { + DONTCARE, // 0 + REQUIRE, // 1 + SUGGEST, // 2 +}; + +pub const PixelFormat = enum(c_int) { + ANY, // 0 + ANY_NO_ALPHA, // 1 + ANY_WITH_ALPHA, // 2 + ANY_15_NO_ALPHA, // 3 + ANY_16_NO_ALPHA, // 4 + ANY_16_WITH_ALPHA, // 5 + ANY_24_NO_ALPHA, // 6 + ANY_32_NO_ALPHA, // 7 + ANY_32_WITH_ALPHA, // 8 + ARGB_8888, // 9 + RGBA_8888, // 10 + ARGB_4444, // 11 + RGB_888, // 12 // 24 bit format + RGB_565, // 13 + RGB_555, // 14 + RGBA_5551, // 15 + ARGB_1555, // 16 + ABGR_8888, // 17 + XBGR_8888, // 18 + BGR_888, // 19 // 24 bit format + BGR_565, // 20 + BGR_555, // 21 + RGBX_8888, // 22 + XRGB_8888, // 23 + ABGR_F32, // 24 + ABGR_8888_LE, // 25 + RGBA_4444, // 26 + SINGLE_CHANNEL_8, // 27 + COMPRESSED_RGBA_DXT1, // 28 + COMPRESSED_RGBA_DXT3, // 29 + COMPRESSED_RGBA_DXT5, // 30 + ALLEGRO_NUM_PIXEL_FORMATS, +}; + +pub const Timer = struct { + native: *c.ALLEGRO_TIMER, + + pub fn destroy(self: Timer) void { + destroyTimer(self); + } + + pub fn eventSource(self: Timer) EventSource { + return getTimerEventSource(self); + } + + pub fn start(self: Timer) void { + startTimer(self); + } +}; + +pub fn acknowledgeResize(display: Display) bool { + return c.al_acknowledge_resize(display.native); +} + +pub fn clearToColor(color: Color) void { + c.al_clear_to_color(color.native); +} + +pub fn convertMemoryBitmaps() void { + c.al_convert_memory_bitmaps(); +} + +pub fn createBitmap(width: i32, height: i32) !Bitmap { + const bitmap = c.al_create_bitmap(width, height); + if (bitmap) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToCreateBitmap; +} + +pub fn createDisplay(width: i32, height: i32) !Display { + const display = c.al_create_display(width, height); + if (display) |native| { + return Display{ .native = native }; + } + return error.FailedToCreateDisplay; +} + +pub fn createEventQueue() !EventQueue { + const queue = c.al_create_event_queue(); + if (queue) |native| { + return EventQueue{ .native = native }; + } + return error.FailedToCreateEventQueue; +} + +pub fn createTimer(interval: f32) !Timer { + const timer = c.al_create_timer(interval); + if (timer) |native| { + return Timer{ .native = native }; + } + return error.FailedToCreateTimer; +} + +pub fn destroyBitmap(bitmap: Bitmap) void { + c.al_destroy_bitmap(bitmap.native); +} + +pub fn destroyDisplay(display: Display) void { + c.al_destroy_display(display.native); +} + +pub fn destroyEventQueue(queue: EventQueue) void { + c.al_destroy_event_queue(queue.native); +} + +pub fn destroyFont(font: Font) void { + c.al_destroy_font(font.native); +} + +pub fn destroyTimer(timer: Timer) void { + c.al_destroy_timer(timer.native); +} + +pub fn drawBitmap(bitmap: Bitmap, dx: f32, dy: f32, flags: DrawFlags) void { + c.al_draw_bitmap(bitmap.native, dx, dy, @bitCast(c_int, flags)); +} + +pub fn drawScaledBitmap(bitmap: Bitmap, sx: f32, sy: f32, sw: f32, sh: f32, dx: f32, dy: f32, dw: f32, dh: f32, flags: DrawFlags) void { + c.al_draw_scaled_bitmap(bitmap.native, sx, sy, sw, sh, dx, dy, dw, dh, @bitCast(c_int, flags)); +} + +pub fn drawText(font: Font, color: Color, x: f32, y: f32, flags: DrawTextFlags, text: [*:0]const u8) void { + c.al_draw_text(font.native, color.native, x, y, @enumToInt(flags), text); +} + +pub fn drawTintedBitmap(bitmap: Bitmap, tint: Color, dx: f32, dy: f32, flags: DrawFlags) void { + c.al_draw_tinted_bitmap(bitmap.native, tint.native, dx, dy, @bitCast(c_int, flags)); +} + +pub fn drawTintedScaledBitmap(bitmap: Bitmap, tint: Color, sx: f32, sy: f32, sw: f32, sh: f32, dx: f32, dy: f32, dw: f32, dh: f32, flags: DrawFlags) void { + c.al_draw_tinted_scaled_bitmap(bitmap.native, tint.native, sx, sy, sw, sh, dx, dy, dw, dh, @bitCast(c_int, flags)); +} + +pub fn dropNextEvent(queue: EventQueue) bool { + return c.al_drop_next_event(queue.native); +} + +pub fn fclose(file: File) bool { + return c.al_fclose(file.native); +} + +pub fn flipDisplay() void { + c.al_flip_display(); +} + +pub fn flushEventQueue(queue: EventQueue) void { + c.al_flush_event_queue(queue.native); +} + +pub fn isEventQueueEmpty(queue: EventQueue) bool { + return c.al_is_event_queue_empty(queue.native); +} + +pub fn getBackbuffer(display: Display) !Bitmap { + const bitmap = c.al_get_backbuffer(display.native); + if (bitmap) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToGetBackBufferForDisplay; +} + +pub fn getBitmapHeight(bitmap: Bitmap) c_int { + return c.al_get_bitmap_height(bitmap.native); +} + +pub fn getBitmapWidth(bitmap: Bitmap) c_int { + return c.al_get_bitmap_width(bitmap.native); +} + +pub fn getCurrentDisplay() !Display { + const display = c.al_get_current_display(); + if (display) |native| { + return Display{ .native = native }; + } + return error.NoCurrentDisplay; +} + +pub fn getDisplayEventSource(display: Display) EventSource { + return EventSource{ .native = c.al_get_display_event_source(display.native) }; +} + +pub fn getDisplayFlags(display: Display) NewDisplayFlags { + return @bitCast(NewDisplayFlags, c.al_get_display_flags(display.native)); +} + +pub fn getDisplayHeight(display: Display) i32 { + return c.al_get_display_height(display.native); +} + +pub fn getDisplayWidth(display: Display) i32 { + return c.al_get_display_width(display.native); +} + +pub fn getFontAscent(font: Font) i32 { + return c.al_get_font_ascent(font.native); +} + +pub fn getFontDescent(font: Font) i32 { + return c.al_get_font_descent(font.native); +} + +pub fn getFontLineHeight(font: Font) i32 { + return c.al_get_font_line_height(font.native); +} + +pub fn getKeyboardEventSource() EventSource { + return EventSource{ .native = c.al_get_keyboard_event_source() }; +} + +pub fn getNewBitmapFlags() NewBitmapFlags { + return c.al_set_new_bitmap_flags(); +} + +pub fn getNewDisplayOption(option: NewDisplayOption, importance: *OptionImportance) c_int { + return c.al_get_new_display_option(@enumToInt(option), @ptrCast(*c_int, importance)); +} + +pub fn getNewBitmapFormat() PixelFormat { + return c.al_set_new_bitmap_format(); +} + +pub fn getNewDisplayFlags() NewDisplayFlags { + return c.al_get_new_display_flags(); +} + +pub fn getNextEvent(queue: EventQueue, event: *Event) bool { + return c.al_get_next_event(queue.native, event); +} + +pub fn getMouseEventSource() EventSource { + return EventSource{ .native = c.al_get_mouse_event_source() }; +} + +pub fn getTextWidth(font: Font, text: [*:0]const u8) void { + c.al_get_text_width(font.native, &text[0]); +} + +pub fn getTime() f64 { + return c.al_get_time(); +} + +pub fn getTimerEventSource(timer: Timer) EventSource { + return EventSource{ .native = c.al_get_timer_event_source(timer.native) }; +} + +pub fn init() bool { + return c.al_init(); +} + +pub fn initFontAddon() bool { + return c.al_init_font_addon(); +} + +pub fn initImageAddon() bool { + return c.al_init_image_addon(); +} + +pub fn initTtfAddon() bool { + return c.al_init_ttf_addon(); +} + +pub fn installKeyboard() bool { + return c.al_install_keyboard(); +} + +pub fn installMouse() bool { + return c.al_install_mouse(); +} + +pub fn loadBitmap(path: [*:0]const u8) !Bitmap { + const bitmap = c.al_load_bitmap(path); + if (bitmap) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToLoadBitmap; +} + +pub fn loadBitmapF(file: File, ident: []const u8) !Bitmap { + const bitmap = c.al_load_bitmap_f(file.native, &ident[0]); + if (bitmap) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToLoadBitmap; +} + +pub fn loadBitmapFont(path: [*:0]const u8) !Font { + const font = c.al_load_bitmap_font(path); + if (font) |native| { + return Font{ .native = native }; + } + return error.FailedToLoadFont; +} + +pub fn loadFont(path: [*:0]const u8, size: i32, flags: c_int) !Font { + const font = c.al_load_font(path, size, flags); + if (font) |native| { + return Font{ .native = native }; + } + return error.FailedToLoadFont; +} + +pub fn loadTtfFont(path: [*:0]const u8, size: i32, flags: LoadTtfFontFlags) !Font { + const font = c.al_load_ttf_font(path, size, @bitCast(c_int, flags)); + if (font) |native| { + return Font{ .native = native }; + } + return error.FailedToLoadFont; +} + +pub fn mapRgb(r: u8, g: u8, b: u8) Color { + return Color{ .native = c.al_map_rgb(r, g, b) }; +} + +pub fn mapRgba(r: u8, g: u8, b: u8, a: u8) Color { + return Color{ .native = c.al_map_rgba(r, g, b, a) }; +} + +pub fn mapRgbaF(r: f32, g: f32, b: f32, a: f32) Color { + return Color{ .native = c.al_map_rgba_f(r, g, b, a) }; +} + +pub fn mapRgbF(r: f32, g: f32, b: f32) Color { + return Color{ .native = c.al_map_rgb_f(r, g, b) }; +} + +pub fn openMemfile(data: []u8, mode: []const u8) !File { + const file = c.al_open_memfile(&data[0], @intCast(i64, data.len), &mode[0]); + if (file) |native| { + return File{ .native = native }; + } + return error.FailedToOpenMemfile; +} + +pub fn peekNextEvent(queue: EventQueue, event: *Event) bool { + return c.al_peek_next_event(queue.native, event); +} + +pub fn registerEventSource(queue: EventQueue, source: EventSource) void { + c.al_register_event_source(queue.native, source.native); +} + +pub fn setDisplayFlag(display: Display, flag: NewDisplayFlags, on: bool) bool { + return c.al_set_display_flag(display.native, @bitCast(c_int, flag), on); +} + +pub fn setNewBitmapFlags(flags: NewBitmapFlags) void { + c.al_set_new_bitmap_flags(@bitCast(c_int, flags)); +} + +pub fn setNewBitmapFormat(format: PixelFormat) void { + c.al_set_new_bitmap_format(format); +} + +pub fn setNewDisplayFlags(flags: NewDisplayFlags) void { + c.al_set_new_display_flags(@bitCast(c_int, flags)); +} + +pub fn setNewDisplayOption(option: NewDisplayOption, value: i32, importance: OptionImportance) void { + c.al_set_new_display_option(@enumToInt(option), value, @enumToInt(importance)); +} + +pub fn setTargetBitmap(bitmap: Bitmap) void { + c.al_set_target_bitmap(bitmap.native); +} + +pub fn setTargetBackbuffer(display: Display) void { + c.al_set_target_bitmap(display.native); +} + +pub fn startTimer(timer: Timer) void { + c.al_start_timer(timer.native); +} + +pub fn unregisterEventSource(queue: EventQueue, source: EventSource) void { + c.al_unregister_event_source(queue.native, source.native); +} + +pub fn waitForEvent(queue: EventQueue, event: *Event) void { + c.al_wait_for_event(queue.native, event); +} + +pub fn waitForVsync() bool { + return c.al_wait_for_vsync(); +} diff --git a/allegro/build.zig b/allegro/build.zig new file mode 100644 index 0000000..75acdb7 --- /dev/null +++ b/allegro/build.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +fn current_file() []const u8 { + return @src().file; +} + +const cwd = std.fs.path.dirname(current_file()).?; +const path_separator = std.fs.path.sep_str; + +/// add this package to exe +pub fn addTo(comptime allegro_dir: []const u8, exe: *std.build.LibExeObjStep) void { + exe.addAnonymousModule("allegro", .{ .source_file = .{ .path = cwd ++ path_separator ++ "allegro.zig" } }); + if (allegro_dir.len > 0) { + exe.addIncludePath(allegro_dir ++ path_separator ++ "include"); + exe.addLibraryPath(allegro_dir ++ path_separator ++ "lib"); + } + exe.linkLibC(); + + exe.linkSystemLibrary("allegro"); + exe.linkSystemLibrary("allegro_font"); + exe.linkSystemLibrary("allegro_image"); + exe.linkSystemLibrary("allegro_memfile"); + exe.linkSystemLibrary("allegro_ttf"); +} diff --git a/allegro/c.zig b/allegro/c.zig new file mode 100644 index 0000000..2f14d1c --- /dev/null +++ b/allegro/c.zig @@ -0,0 +1,8 @@ +pub const c = @cImport({ + @cInclude("stdint.h"); + @cInclude("allegro5/allegro.h"); + @cInclude("allegro5/allegro_font.h"); + @cInclude("allegro5/allegro_image.h"); + @cInclude("allegro5/allegro_memfile.h"); + @cInclude("allegro5/allegro_ttf.h"); +}); diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..1d79485 --- /dev/null +++ b/build.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const allegro = @import("allegro/build.zig"); + +const allegroPath = @embedFile("allegro.path"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "tins2023", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + allegro.addTo(allegroPath, exe); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/src/assets/fonts/Cabin-OFL.txt b/src/assets/fonts/Cabin-OFL.txt new file mode 100644 index 0000000..27f0a41 --- /dev/null +++ b/src/assets/fonts/Cabin-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Cabin Project Authors (https://github.com/impallari/Cabin.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/assets/fonts/Cabin-Regular.ttf b/src/assets/fonts/Cabin-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5ca7f6f088ca7d7c24cff7f486b75bc1aa07b728 GIT binary patch literal 74556 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/opaque.png b/src/assets/images/opaque.png new file mode 100644 index 0000000000000000000000000000000000000000..dee54f46bf7af66204b75de3ac3b608378cd3a3f GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFzopr0D!e3%K!iX literal 0 HcmV?d00001 diff --git a/src/assets/images/title_untitled.png b/src/assets/images/title_untitled.png new file mode 100644 index 0000000000000000000000000000000000000000..46f341918096e359386c1a3f26637564727c5d4e GIT binary patch literal 11614 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 literal 0 HcmV?d00001 diff --git a/src/context.zig b/src/context.zig new file mode 100644 index 0000000..9051967 --- /dev/null +++ b/src/context.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const allegro = @import("allegro"); +const engine = @import("engine.zig"); +const Palette = @import("palette.zig").Palette; +const Scene = @import("scene.zig").Scene; + +pub const Context = struct { + const DefaultDisplayWidth = 1280; + const DefaultDisplayHeight = 720; + + allocator: std.mem.Allocator, + palette: Palette = undefined, + shouldQuit: bool = false, + scene: ?Scene = null, + + display: allegro.Display, + events: allegro.EventQueue, + + viewport: engine.Viewport, + fonts: engine.Fonts, + textures: engine.Textures, + + pub fn init(allocator: std.mem.Allocator) !Context { + _ = allegro.init(); + _ = allegro.initImageAddon(); + _ = allegro.installKeyboard(); + _ = allegro.installMouse(); + _ = allegro.initFontAddon(); + _ = allegro.initTtfAddon(); + + allegro.setNewDisplayOption(allegro.NewDisplayOption.VSYNC, 1, allegro.OptionImportance.REQUIRE); + allegro.setNewDisplayOption(allegro.NewDisplayOption.SAMPLE_BUFFERS, 1, allegro.OptionImportance.REQUIRE); + allegro.setNewDisplayOption(allegro.NewDisplayOption.SAMPLES, 4, allegro.OptionImportance.REQUIRE); + allegro.setNewDisplayFlags(allegro.NewDisplayFlags{ .RESIZABLE = true }); + + const viewport = engine.Viewport.init(DefaultDisplayWidth, DefaultDisplayHeight); + const display = try allegro.createDisplay(DefaultDisplayWidth, DefaultDisplayHeight); + + const events = try allegro.createEventQueue(); + + events.registerDisplay(display); + events.registerKeyboard(); + events.registerMouse(); + + return Context{ + .allocator = allocator, + .display = display, + .events = events, + .viewport = viewport, + .fonts = engine.Fonts.init(allocator), + .textures = engine.Textures.init(allocator), + }; + } + + fn exitScene(self: *Context) void { + if (self.scene) |scene| { + scene.exit(self); + scene.deinit(); + self.scene = null; + } + } + + pub fn deinit(self: *Context) void { + self.events.destroy(); + self.display.destroy(); + self.exitScene(); + self.fonts.deinit(); + self.textures.deinit(); + } + + pub fn quit(self: *Context) void { + self.shouldQuit = true; + } + + pub fn resized(self: *Context, width: i32, height: i32) void { + self.viewport.update(width, height); + } + + pub fn switchToScene(self: *Context, comptime SceneType: type, build: ?*const fn (*SceneType) void) !void { + self.exitScene(); + + const scene = try Scene.init(SceneType, self.allocator, build); + self.scene = scene; + scene.enter(self); + } + + pub fn toggleFullScreen(self: *Context) void { + var displayFlags = allegro.getDisplayFlags(self.display); + _ = allegro.setDisplayFlag(self.display, allegro.NewDisplayFlags{ .FULLSCREEN_WINDOW = true }, !displayFlags.FULLSCREEN_WINDOW); + self.viewport.update(self.display.width(), self.display.height()); + } +}; diff --git a/src/engine.zig b/src/engine.zig new file mode 100644 index 0000000..5ad1421 --- /dev/null +++ b/src/engine.zig @@ -0,0 +1,11 @@ +const assets = @import("engine/assets.zig"); + +pub const Fonts = assets.Fonts; +pub const OpaquePtr = @import("engine/opaque_ptr.zig").OpaquePtr; +pub const Point = @import("engine/point.zig").Point; +pub const PointF = @import("engine/point_f.zig").PointF; +pub const Rectangle = @import("engine/rectangle.zig").Rectangle; +pub const RectangleF = @import("engine/rectangle_f.zig").RectangleF; +pub const Scene = @import("engine/scene.zig").Scene; +pub const Textures = assets.Textures; +pub const Viewport = @import("engine/viewport.zig").Viewport; diff --git a/src/engine/assets.zig b/src/engine/assets.zig new file mode 100644 index 0000000..a5d31cf --- /dev/null +++ b/src/engine/assets.zig @@ -0,0 +1,97 @@ +const std = @import("std"); +const allegro = @import("allegro"); + +pub fn Assets(comptime Asset: type, comptime destroy: *const fn (Asset) void) type { + return struct { + const Self = @This(); + + assets: std.StringHashMap(Asset), + + pub fn init(allocator: std.mem.Allocator) Self { + return Self{ + .assets = std.StringHashMap(Asset).init(allocator), + }; + } + + pub fn deinit(self: *Self) void { + var assets = self.assets.iterator(); + while (assets.next()) |entry| { + destroy(entry.value_ptr.*); + } + self.assets.deinit(); + } + + pub fn get(self: Self, name: []const u8) ?Asset { + return self.assets.get(name); + } + + pub fn put(self: *Self, name: []const u8, asset: Asset) !void { + try self.assets.put(name, asset); + } + }; +} + +pub const Fonts = struct { + fn destroy(font: allegro.Font) void { + font.destroy(); + } + + const Map = Assets(allegro.Font, Fonts.destroy); + + assets: Map, + + pub fn init(allocator: std.mem.Allocator) Fonts { + return Fonts{ + .assets = Map.init(allocator), + }; + } + + pub fn deinit(self: *Fonts) void { + self.assets.deinit(); + } + + pub fn addFromFileTTF(self: *Fonts, name: []const u8, path: [*:0]const u8, size: i32) !void { + const font = try allegro.loadTtfFont(path, size, allegro.LoadTtfFontFlags{}); + try self.assets.put(name, font); + } + + pub fn get(self: Fonts, name: []const u8) ?allegro.Font { + return self.assets.get(name); + } +}; + +pub const Textures = struct { + fn destroy(bitmap: allegro.Bitmap) void { + bitmap.destroy(); + } + const Map = Assets(allegro.Bitmap, destroy); + + assets: Map, + + pub fn init(allocator: std.mem.Allocator) Textures { + return Textures{ + .assets = Map.init(allocator), + }; + } + + pub fn deinit(self: *Textures) void { + self.assets.deinit(); + } + + pub fn addFromFile(self: *Textures, name: []const u8, path: [*:0]const u8) !void { + const bitmap = try allegro.loadBitmap(path); + try self.assets.put(name, bitmap); + } + + pub fn addFromMemoryPNG(self: *Textures, name: []const u8, data: []const u8) !void { + const memoryFile = try allegro.openMemfile(@constCast(data), "r"); + defer _ = allegro.fclose(memoryFile); + + const bitmap = try allegro.loadBitmapF(memoryFile, ".png"); + try self.assets.put(name, bitmap); + } + + pub fn get(self: Textures, name: []const u8) ?allegro.Bitmap { + return self.assets.get(name); + } +}; diff --git a/src/engine/opaque_ptr.zig b/src/engine/opaque_ptr.zig new file mode 100644 index 0000000..3920574 --- /dev/null +++ b/src/engine/opaque_ptr.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub const OpaquePtr = struct { + allocator: Allocator, + object: usize = undefined, + virtualTable: VirtualTable = undefined, + + const VirtualTable = struct { + destroy: *const fn (OpaquePtr) void, + }; + + pub fn Object(comptime Type: type) type { + return struct { + ptr: *Type, + opaquePtr: OpaquePtr, + }; + } + + pub fn create(comptime Type: type, allocator: Allocator) !Object(Type) { + const ptr = try allocator.create(Type); + const opaq = OpaquePtr{ + .allocator = allocator, + .object = @ptrToInt(ptr), + .virtualTable = .{ + .destroy = struct { + fn destroy(self: OpaquePtr) void { + const object = @intToPtr(*Type, self.object); + self.allocator.destroy(object); + } + }.destroy, + }, + }; + return Object(Type){ + .ptr = ptr, + .opaquePtr = opaq, + }; + } + + pub fn destroy(self: OpaquePtr) void { + self.virtualTable.destroy(self); + } +}; diff --git a/src/engine/point.zig b/src/engine/point.zig new file mode 100644 index 0000000..11944af --- /dev/null +++ b/src/engine/point.zig @@ -0,0 +1,30 @@ +const PointF = @import("point_f.zig").PointF; + +pub const Point = struct { + x: i64, + y: i64, + + pub fn init(x: i64, y: i64) Point { + return Point{ .x = x, .y = y }; + } + + pub fn initUsize(x: usize, y: usize) Point { + return Point.init(@intCast(i64, x), @intCast(i64, y)); + } + + pub fn add(self: Point, other: Point) Point { + return Point{ .x = self.x + other.x, .y = self.y + other.y }; + } + + pub fn float(self: Point) PointF { + return PointF{ .x = @intToFloat(f32, self.x), .y = @intToFloat(f32, self.y) }; + } + + pub fn multiply(self: Point, factor: i64) Point { + return Point{ .x = self.x * factor, .y = self.y * factor }; + } + + pub fn subtract(self: Point, other: Point) Point { + return Point{ .x = self.x - other.x, .y = self.y - other.y }; + } +}; diff --git a/src/engine/point_f.zig b/src/engine/point_f.zig new file mode 100644 index 0000000..2746fdd --- /dev/null +++ b/src/engine/point_f.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +const Point = @import("point.zig").Point; + +pub const PointF = struct { + x: f32, + y: f32, + + pub fn init(x: f32, y: f32) PointF { + return PointF{ .x = x, .y = y }; + } + + pub fn add(self: PointF, other: PointF) PointF { + return PointF{ .x = self.x + other.x, .y = self.y + other.y }; + } + + pub fn ceil(self: PointF) PointF { + return PointF{ .x = std.math.ceil(self.x), .y = std.math.ceil(self.y) }; + } + + pub fn floor(self: PointF) PointF { + return PointF{ .x = std.math.floor(self.x), .y = std.math.floor(self.y) }; + } + + pub fn integer(self: PointF) Point { + return Point{ .x = @floatToInt(i64, self.x), .y = @floatToInt(i64, self.y) }; + } + + pub fn multiply(self: PointF, factor: f32) PointF { + return PointF{ .x = self.x * factor, .y = self.y * factor }; + } + + pub fn subtract(self: PointF, other: PointF) PointF { + return PointF{ .x = self.x - other.x, .y = self.y - other.y }; + } +}; diff --git a/src/engine/rectangle.zig b/src/engine/rectangle.zig new file mode 100644 index 0000000..ef38cee --- /dev/null +++ b/src/engine/rectangle.zig @@ -0,0 +1,75 @@ +const Point = @import("point.zig").Point; + +pub const Rectangle = struct { + min: Point, + max: Point, + + pub fn init(min: Point, max: Point) Rectangle { + if (min.x > max.x) unreachable; + if (min.y > max.y) unreachable; + + return Rectangle{ .min = min, .max = max }; + } + + pub fn initAbsolute(x1: i64, y1: i64, x2: i64, y2: i64) Rectangle { + if (x1 < x2) { + if (y1 < y2) { + return Rectangle{ + .min = Point{ .x = x1, .y = y1 }, + .max = Point{ .x = x2, .y = y2 }, + }; + } + return Rectangle{ + .min = Point{ .x = x1, .y = y2 }, + .max = Point{ .x = x2, .y = y1 }, + }; + } + if (y1 < y2) { + return Rectangle{ + .min = Point{ .x = x2, .y = y1 }, + .max = Point{ .x = x1, .y = y2 }, + }; + } + return Rectangle{ + .min = Point{ .x = x2, .y = y2 }, + .max = Point{ .x = x1, .y = y1 }, + }; + } + + pub fn initRelative(x: i64, y: i64, w: i64, h: i64) Rectangle { + if (w < 0) { + if (h < 0) { + return Rectangle{ + .min = Point{ .x = x + w, .y = y + h }, + .max = Point{ .x = x, .y = y }, + }; + } + return Rectangle{ + .min = Point{ .x = x + w, .y = y }, + .max = Point{ .x = x, .y = y + h }, + }; + } + if (h < 0) { + return Rectangle{ + .min = Point{ .x = x, .y = y + h }, + .max = Point{ .x = x + w, .y = y }, + }; + } + return Rectangle{ + .min = Point{ .x = x, .y = y }, + .max = Point{ .x = x + w, .y = y + h }, + }; + } + + pub fn center(self: Rectangle) Point { + return Point.init(@divTrunc(self.min.x + self.max.x, 2), @divTrunc(self.min.y + self.max.y, 2)); + } + + pub fn height(self: Rectangle) i64 { + return self.max.y - self.min.y; + } + + pub fn width(self: Rectangle) i64 { + return self.max.x - self.min.x; + } +}; diff --git a/src/engine/rectangle_f.zig b/src/engine/rectangle_f.zig new file mode 100644 index 0000000..6dd3abe --- /dev/null +++ b/src/engine/rectangle_f.zig @@ -0,0 +1,75 @@ +const PointF = @import("point_f.zig").PointF; + +pub const RectangleF = struct { + min: PointF, + max: PointF, + + pub fn init(min: PointF, max: PointF) RectangleF { + if (min.x > max.x) unreachable; + if (min.y > max.y) unreachable; + + return RectangleF{ .min = min, .max = max }; + } + + pub fn initAbsolute(x1: f32, y1: f32, x2: f32, y2: f32) RectangleF { + if (x1 < x2) { + if (y1 < y2) { + return RectangleF{ + .min = PointF{ .x = x1, .y = y1 }, + .max = PointF{ .x = x2, .y = y2 }, + }; + } + return RectangleF{ + .min = PointF{ .x = x1, .y = y2 }, + .max = PointF{ .x = x2, .y = y1 }, + }; + } + if (y1 < y2) { + return RectangleF{ + .min = PointF{ .x = x2, .y = y1 }, + .max = PointF{ .x = x1, .y = y2 }, + }; + } + return RectangleF{ + .min = PointF{ .x = x2, .y = y2 }, + .max = PointF{ .x = x1, .y = y1 }, + }; + } + + pub fn initRelative(x: f32, y: f32, w: f32, h: f32) RectangleF { + if (w < 0) { + if (h < 0) { + return RectangleF{ + .min = PointF{ .x = x + w, .y = y + h }, + .max = PointF{ .x = x, .y = y }, + }; + } + return RectangleF{ + .min = PointF{ .x = x + w, .y = y }, + .max = PointF{ .x = x, .y = y + h }, + }; + } + if (h < 0) { + return RectangleF{ + .min = PointF{ .x = x, .y = y + h }, + .max = PointF{ .x = x + w, .y = y }, + }; + } + return RectangleF{ + .min = PointF{ .x = x, .y = y }, + .max = PointF{ .x = x + w, .y = y + h }, + }; + } + + pub fn center(self: RectangleF) PointF { + return self.min.add(self.max).multiply(0.5); + } + + pub fn height(self: RectangleF) f32 { + return self.max.y - self.min.y; + } + + pub fn width(self: RectangleF) f32 { + return self.max.x - self.min.x; + } +}; diff --git a/src/engine/scene.zig b/src/engine/scene.zig new file mode 100644 index 0000000..ea026e3 --- /dev/null +++ b/src/engine/scene.zig @@ -0,0 +1,90 @@ +const std = @import("std"); +const OpaquePtr = @import("opaque_ptr.zig").OpaquePtr; + +pub fn Scene(comptime Context: type, comptime Event: type) type { + return struct { + const Self = @This(); + + object: usize = undefined, + virtualTable: VirtualTable = undefined, + opaquePtr: ?OpaquePtr = null, + + const VirtualTable = struct { + enter: *const fn (Self, *Context) void, + exit: *const fn (Self, *Context) void, + handle: *const fn (Self, *Context, Event) anyerror!void, + update: *const fn (Self, *Context, f32) void, + render: *const fn (Self, *Context) void, + }; + + pub fn enter(self: Self, ctx: *Context) void { + self.virtualTable.enter(self, ctx); + } + + pub fn exit(self: Self, ctx: *Context) void { + self.virtualTable.exit(self, ctx); + } + + pub fn handle(self: Self, ctx: *Context, event: Event) !void { + try self.virtualTable.handle(self, ctx, event); + } + + pub fn update(self: Self, ctx: *Context, dt: f32) void { + self.virtualTable.update(self, ctx, dt); + } + + pub fn render(self: Self, ctx: *Context) void { + self.virtualTable.render(self, ctx); + } + + pub fn makeOpaque(comptime SceneType: type, object: *SceneType) Self { + return Self{ .object = @ptrToInt(object), .virtualTable = .{ + .enter = struct { + fn enter(self: Self, ctx: *Context) void { + const scene = @intToPtr(*SceneType, self.object); + scene.enter(ctx); + } + }.enter, + .exit = struct { + fn exit(self: Self, ctx: *Context) void { + const scene = @intToPtr(*SceneType, self.object); + scene.exit(ctx); + } + }.exit, + .handle = struct { + fn handle(self: Self, ctx: *Context, event: Event) !void { + const scene = @intToPtr(*SceneType, self.object); + try scene.handle(ctx, event); + } + }.handle, + .update = struct { + fn update(self: Self, ctx: *Context, dt: f32) void { + const scene = @intToPtr(*SceneType, self.object); + scene.update(ctx, dt); + } + }.update, + .render = struct { + fn render(self: Self, ctx: *Context) void { + const scene = @intToPtr(*SceneType, self.object); + scene.render(ctx); + } + }.render, + } }; + } + + pub fn init(comptime SceneType: type, allocator: std.mem.Allocator, build: ?*const fn (*SceneType) void) !Self { + const object = try OpaquePtr.create(SceneType, allocator); + var scene = Self.makeOpaque(SceneType, object.ptr); + if (build) |b| { + b(object.ptr); + } + return scene; + } + + pub fn deinit(self: Self) void { + if (self.opaquePtr) |ptr| { + ptr.destroy(); + } + } + }; +} diff --git a/src/engine/viewport.zig b/src/engine/viewport.zig new file mode 100644 index 0000000..c53e34d --- /dev/null +++ b/src/engine/viewport.zig @@ -0,0 +1,54 @@ +const PointF = @import("point_f.zig").PointF; +const RectangleF = @import("rectangle_f.zig").RectangleF; + +pub const Viewport = struct { + unscaledWidth: i32, + unscaledHeight: i32, + + actualWidth: i32, + actualHeight: i32, + + scaledWidth: i32, + scaledHeight: i32, + bounds: RectangleF, + scale: f32, + + pub fn init(width: i32, height: i32) Viewport { + return Viewport{ + .unscaledWidth = width, + .unscaledHeight = height, + .actualWidth = width, + .actualHeight = height, + .scaledWidth = width, + .scaledHeight = height, + .bounds = RectangleF.initRelative(0, 0, @intToFloat(f32, width), @intToFloat(f32, height)), + .scale = 1.0, + }; + } + + pub fn center(self: Viewport) PointF { + return self.bounds.center(); + } + + pub fn update(self: *Viewport, width: i32, height: i32) void { + self.actualWidth = width; + self.actualHeight = height; + + const horizontalRatio = @intToFloat(f32, width) / @intToFloat(f32, self.unscaledWidth); + const verticalRatio = @intToFloat(f32, height) / @intToFloat(f32, self.unscaledHeight); + + if (horizontalRatio < verticalRatio) { + self.scaledWidth = width; + self.scaledHeight = @floatToInt(i32, horizontalRatio * @intToFloat(f32, self.unscaledHeight)); + const top = @divFloor(height - self.scaledHeight, 2); + self.bounds = RectangleF.initRelative(0, @intToFloat(f32, top), @intToFloat(f32, width), @intToFloat(f32, self.scaledHeight)); + self.scale = horizontalRatio; + } else { + self.scaledWidth = @floatToInt(i32, verticalRatio * @intToFloat(f32, self.unscaledWidth)); + self.scaledHeight = height; + const left = @divFloor(width - self.scaledWidth, 2); + self.bounds = RectangleF.initRelative(@intToFloat(f32, left), 0, @intToFloat(f32, self.scaledWidth), @intToFloat(f32, height)); + self.scale = verticalRatio; + } + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..ad4e4ed --- /dev/null +++ b/src/main.zig @@ -0,0 +1,98 @@ +const std = @import("std"); +const allegro = @import("allegro"); +const engine = @import("engine.zig"); +const Context = @import("context.zig").Context; +const Palette = @import("palette.zig").Palette; +const TitleScene = @import("title_scene.zig").TitleScene; + +fn currentFile() []const u8 { + return @src().file; +} + +const sourceDir = std.fs.path.dirname(currentFile()).?; +const assetsDir = sourceDir ++ "/assets"; + +fn hexColor(hex: []const u8) allegro.Color { + return allegro.Color.initFromHex(hex); +} + +pub fn main() !void { + // Primary: #551485 + // Secondary: #e44b50 + // Tertiary: #479be6 + // Neutral: #948f94 + + var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var context = try Context.init(allocator); + defer context.deinit(); + context.palette = Palette{ + .primary = .{ .background = hexColor("#7d42ad"), .text = hexColor("#ffffff") }, + .primaryContainer = .{ .background = hexColor("#f2daff"), .text = hexColor("#2e004e") }, + .secondary = .{ .background = hexColor("#b32731"), .text = hexColor("#ffffff") }, + .secondaryContainer = .{ .background = hexColor("#ffdad8"), .text = hexColor("#410006") }, + .tertiary = .{ .background = hexColor("#0062a0"), .text = hexColor("#ffffff") }, + .tertiaryContainer = .{ .background = hexColor("#d0e4ff"), .text = hexColor("#001d35") }, + .background = .{ .background = hexColor("#fffbff"), .text = hexColor("#2b0052") }, + .outline = hexColor("#7c757e"), + }; + + allegro.setNewBitmapFlags(allegro.NewBitmapFlags{ .MIN_LINEAR = true, .MAG_LINEAR = true }); + try context.textures.addFromFile("opaque", assetsDir ++ "/images/opaque.png"); + try context.textures.addFromFile("title_untitled", assetsDir ++ "/images/title_untitled.png"); + allegro.convertMemoryBitmaps(); + + try context.fonts.addFromFileTTF("sub", assetsDir ++ "/fonts/Cabin-Regular.ttf", 16); + try context.fonts.addFromFileTTF("default", assetsDir ++ "/fonts/Cabin-Regular.ttf", 32); + + try context.switchToScene(TitleScene, null); + + var t = allegro.getTime(); + while (!context.shouldQuit) { + const scene = if (context.scene) |scene| scene else { + break; + }; + + while (!context.events.isEmpty()) { + var event: allegro.Event = undefined; + _ = context.events.get(&event); + switch (event.type) { + allegro.c.ALLEGRO_EVENT_DISPLAY_CLOSE => context.quit(), + allegro.c.ALLEGRO_EVENT_DISPLAY_RESIZE => { + context.resized(event.display.width, event.display.height); + _ = allegro.acknowledgeResize(allegro.Display{ .native = event.display.source.? }); + }, + allegro.c.ALLEGRO_EVENT_KEY_CHAR => { + switch (event.keyboard.keycode) { + allegro.c.ALLEGRO_KEY_ESCAPE => context.quit(), + allegro.c.ALLEGRO_KEY_F11 => context.toggleFullScreen(), + else => {}, + } + }, + else => {}, + } + if (context.shouldQuit) { + break; + } + try scene.handle(&context, &event); + } + + const newT = allegro.getTime(); + const deltaT = @floatCast(f32, newT - t); + t = newT; + scene.update(&context, deltaT); + + scene.render(&context); + + allegro.flipDisplay(); + } +} + +test "simple test" { + var list = std.ArrayList(i32).init(std.testing.allocator); + defer list.deinit(); // try commenting this out and see if zig detects the memory leak! + try list.append(42); + try std.testing.expectEqual(@as(i32, 42), list.pop()); +} diff --git a/src/main_menu_scene.zig b/src/main_menu_scene.zig new file mode 100644 index 0000000..a126546 --- /dev/null +++ b/src/main_menu_scene.zig @@ -0,0 +1,36 @@ +const allegro = @import("allegro"); +const Context = @import("context.zig").Context; + +pub const MainMenuScene = struct { + pub fn enter(self: *MainMenuScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn exit(self: *MainMenuScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn handle(self: *MainMenuScene, ctx: *Context, event: *allegro.Event) !void { + _ = event; + _ = ctx; + _ = self; + } + + pub fn update(self: *MainMenuScene, ctx: *Context, dt: f32) void { + _ = self; + _ = dt; + _ = ctx; + } + + pub fn render(self: *MainMenuScene, ctx: *Context) void { + _ = self; + allegro.clearToColor(ctx.palette.background.background); + + const center = ctx.viewport.center(); + ctx.textures.get("title_untitled").?.drawTintedCenteredScaled(ctx.palette.background.text, center.x, ctx.viewport.bounds.min.y + ctx.viewport.scale * 100, 0.5 * ctx.viewport.scale); + + allegro.drawText(ctx.fonts.get("default").?, ctx.palette.background.text, center.x, ctx.viewport.bounds.min.y + ctx.viewport.scale * 200, allegro.DrawTextFlags.ALIGN_CENTER, "Play game"); + } +}; diff --git a/src/palette.zig b/src/palette.zig new file mode 100644 index 0000000..4e6ff82 --- /dev/null +++ b/src/palette.zig @@ -0,0 +1,14 @@ +const allegro = @import("allegro"); + +pub const Palette = struct { + pub const Combination = struct { background: allegro.Color, text: allegro.Color }; + + primary: Combination, + primaryContainer: Combination, + secondary: Combination, + secondaryContainer: Combination, + tertiary: Combination, + tertiaryContainer: Combination, + background: Combination, + outline: allegro.Color, +}; diff --git a/src/scene.zig b/src/scene.zig new file mode 100644 index 0000000..afbc46d --- /dev/null +++ b/src/scene.zig @@ -0,0 +1,5 @@ +const allegro = @import("allegro"); +const engine = @import("engine.zig"); +const Context = @import("context.zig").Context; + +pub const Scene = engine.Scene(Context, *allegro.Event); diff --git a/src/title_scene.zig b/src/title_scene.zig new file mode 100644 index 0000000..a1fadda --- /dev/null +++ b/src/title_scene.zig @@ -0,0 +1,45 @@ +const allegro = @import("allegro"); +const Context = @import("context.zig").Context; +const MainMenuScene = @import("main_menu_scene.zig").MainMenuScene; + +pub const TitleScene = struct { + offset: f32, + + pub fn enter(self: *TitleScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn exit(self: *TitleScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn handle(self: *TitleScene, ctx: *Context, event: *allegro.Event) !void { + switch (event.type) { + allegro.c.ALLEGRO_EVENT_KEY_CHAR => { + switch (event.keyboard.keycode) { + allegro.c.ALLEGRO_KEY_SPACE => try ctx.switchToScene(MainMenuScene, null), + else => {}, + } + }, + else => {}, + } + _ = self; + } + + pub fn update(self: *TitleScene, ctx: *Context, dt: f32) void { + _ = self; + _ = dt; + _ = ctx; + } + + pub fn render(self: *TitleScene, ctx: *Context) void { + _ = self; + allegro.clearToColor(ctx.palette.background.background); + ctx.textures.get("opaque").?.drawTintedScaled(ctx.palette.tertiaryContainer.background, ctx.viewport.bounds.min.x, ctx.viewport.bounds.min.y, ctx.viewport.bounds.width(), ctx.viewport.bounds.height(), allegro.DrawFlags{}); + + const center = ctx.viewport.center(); + ctx.textures.get("title_untitled").?.drawTintedCenteredScaled(ctx.palette.background.text, center.x, center.y, ctx.viewport.scale); + } +};