From 7716ddf26a7832e9417c2f60f85b9af92d7eefd7 Mon Sep 17 00:00:00 2001
From: Nathan Anderson <n8r@tuta.io>
Date: Thu, 9 Jan 2025 14:35:52 -0700
Subject: [PATCH] Leaky mess rn...

---
 src/game_state.zig    | 151 ++++++++++++++++++++++++++++++++-------
 src/main.zig          |  94 +++++++++++++++++--------
 src/slime_factory.zig |  44 +++++++++---
 src/text.zig          | 160 ++++++++++++++++++++++++++----------------
 src/utils/offset.zig  |   4 +-
 src/utils/timer.zig   |   1 +
 6 files changed, 327 insertions(+), 127 deletions(-)

diff --git a/src/game_state.zig b/src/game_state.zig
index 6edc9ca..dd5e42a 100644
--- a/src/game_state.zig
+++ b/src/game_state.zig
@@ -2,30 +2,64 @@ const sdl = @import("./sdl.zig").c;
 const std = @import("std");
 const sf = @import("./slime_factory.zig");
 const atils = @import("./asset_utils.zig");
-const GameText = @import("./text.zig").GameText;
+const text = @import("./text.zig");
 const RGBAColor = @import("./utils/rgb_color.zig").RGBAColor;
+const Timer = @import("./utils/timer.zig").Timer;
 
-pub const GameState = struct {
-    r: u8 = 0xff,
-    g: u8 = 0xff,
-    b: u8 = 0xff,
+const SCREEN_FPS: f64 = 30.0;
+const SCREEN_TICKS_PER_FRAME: f64 = 1000 / SCREEN_FPS;
+const PHYS_UPS: f64 = 310.0;
+const PHYS_TICKS_PER_UPDATE: f64 = 1000 / PHYS_UPS;
+const PHYS_UPDATES_PER_RENDER: f64 = PHYS_UPS / SCREEN_FPS;
+const phys_updates_per_render: u32 = @intFromFloat(PHYS_UPDATES_PER_RENDER);
+
+pub const GameStateController = struct {
     SCREEN_WIDTH: u32 = 640,
     SCREEN_HEIGHT: u32 = 480,
-    slime_factory: sf.SlimeFactory = undefined,
+    slime_factory_controller: sf.SlimeFactoryController = undefined,
+    game_text_factory_controller: text.GameTextFactoryController = undefined,
+    prev_game_state: *GameState = undefined,
+    current_game_state: *GameState = undefined,
     renderer: *sdl.struct_SDL_Renderer = undefined,
+    rng: std.Random = undefined,
+    allocator: std.mem.Allocator = undefined,
+    fps_timer: Timer,
+    phys_timer: Timer,
+    frames: u32 = 0,
+    phys_updates_since_last_render: u16 = 0,
 
-    pub fn init(allocator: std.mem.Allocator, renderer: *sdl.struct_SDL_Renderer) !GameState {
-        var slime_factory = try sf.SlimeFactory.init(allocator, renderer);
+    pub fn init(allocator: std.mem.Allocator, renderer: *sdl.struct_SDL_Renderer) !GameStateController {
+        var slime_factory = try sf.SlimeFactoryController.init(allocator, renderer);
+        const text_factory = try text.GameTextFactoryController.init(allocator);
+        var xosh = std.rand.DefaultPrng.init(0);
         try slime_factory.add(.{});
-        return .{ .slime_factory = slime_factory, .renderer = renderer };
+        var timer = Timer{};
+        timer.start();
+        var phys_timer = Timer{};
+        phys_timer.start();
+
+        return .{
+            .slime_factory_controller = slime_factory,
+            .game_text_factory_controller = text_factory,
+            .renderer = renderer,
+            .rng = xosh.random(),
+            .allocator = allocator,
+            .fps_timer = timer,
+            .phys_timer = phys_timer,
+        };
     }
 
-    pub fn deinit(self: *GameState) void {
-        self.slime_factory.deinit();
+    pub fn deinit(self: *GameStateController) void {
+        self.slime_factory_controller.deinit();
     }
 
     // Physics main loop that processes variable time steps
-    pub fn update_tick(self: *GameState) !void {
+    pub fn update_tick(self: *GameStateController) !*const GameState {
+        // DEBUG random wait time
+        // std.debug.print("getting rng\n", .{});
+        // const wait_time_ms: u32 = 6; //self.rng.int(u32);
+        // sdl.SDL_Delay(wait_time_ms % 15);
+        // DEFINITELY REMOVE BEFORE PROD
         const bg_texture = try atils.loadTexture(self.renderer, "assets/background.png");
         defer sdl.SDL_DestroyTexture(bg_texture);
 
@@ -33,16 +67,16 @@ pub const GameState = struct {
         _ = sdl.SDL_RenderClear(self.renderer);
         _ = sdl.SDL_RenderCopy(self.renderer, bg_texture, null, null);
 
-        var text = try GameText.loadFromRenderedText("Press S to start / stop", RGBAColor.whiteSmoke().tosdl());
-        try text.render(
-            self,
-            .{ .x = @intCast(self.SCREEN_WIDTH - text.w), .y = 2 },
-        );
-        text = try GameText.loadFromRenderedText("Press P to pause / unpause", RGBAColor.whiteSmoke().tosdl());
-        try text.render(
-            self,
-            .{ .x = @intCast(self.SCREEN_WIDTH - text.w), .y = 24 },
-        );
+        // var text = try GameText.loadFromRenderedText("Press S to start / stop", RGBAColor.whiteSmoke().tosdl());
+        // try text.render(
+        //     self,
+        //     .{ .x = @intCast(self.SCREEN_WIDTH - text.w), .y = 2 },
+        // );
+        // text = try GameText.loadFromRenderedText("Press P to pause / unpause", RGBAColor.whiteSmoke().tosdl());
+        // try text.render(
+        //     self,
+        //     .{ .x = @intCast(self.SCREEN_WIDTH - text.w), .y = 24 },
+        // );
 
         // var buf: [16]u8 = undefined;
         // const timer_ms = timer.getTicks();
@@ -79,11 +113,78 @@ pub const GameState = struct {
         // _ = sdl.SDL_UpdateWindowSurface(window);
         // sdl.SDL_Delay(100);
 
-        self.slime_factory.render(self);
+        var game_state = try self.allocator.create(GameState);
+        game_state.allocator = self.allocator;
+        game_state.slime_factory = try self.slime_factory_controller.spawnFactory(self.allocator);
+        game_state.fps_timer = &self.fps_timer;
+        game_state.phys_ticks = @floatFromInt(self.phys_timer.getTicks());
+        game_state.phys_updates_since_render = self.phys_updates_since_last_render;
+
+        // self.allocator.destroy(self.prev_game_state);
+        self.prev_game_state = self.current_game_state;
+        self.current_game_state = game_state;
+        self.phys_updates_since_last_render += 1;
+
+        return game_state;
     }
 
     // Renders the current frame to screen
-    pub fn render(self: *GameState) void {
-        _ = sdl.SDL_RenderPresent(self.renderer);
+    pub fn render(self: *GameStateController, game_state: *const GameState) !void {
+        try self.game_text_factory_controller.addText(.{ .text = "Test new text factory", .offset = .{ .x = 15, .y = 15 } });
+        var buf: [8]u8 = undefined;
+        const fps_text = try self.getFpsText(&buf);
+        try self.game_text_factory_controller.addText(.{ .text = fps_text, .offset = .{ .x = 400, .y = 15 } });
+
+        try self.game_text_factory_controller.render(self.renderer, game_state);
+        try self.slime_factory_controller.render(self.renderer);
+
+        sdl.SDL_RenderPresent(self.renderer);
+        self.frames += 1;
+        self.phys_updates_since_last_render = 0;
+        self.phys_timer.reset();
+    }
+
+    fn getFpsText(self: *GameStateController, buf: []u8) ![:0]const u8 {
+        const ticks_f: f64 = @floatFromInt(self.fps_timer.getTicks());
+        const frames_f: f64 = @floatFromInt(self.frames);
+        const avg_fps: f64 = @round(frames_f / (ticks_f / 1000.0));
+        return try std.fmt.bufPrintZ(buf, "{d} fps", .{avg_fps});
+    }
+};
+
+pub const GameState = struct {
+    allocator: std.mem.Allocator,
+    slime_factory: sf.SlimeFactory = undefined,
+    fps_timer: *Timer,
+    phys_ticks: f64,
+    phys_updates_since_render: u32,
+    // Checks all struct fields in GameState and calls deinit
+    pub fn deinit(self: *const GameState) void {
+        const game_state_info = @typeInfo(GameState);
+        switch (game_state_info) {
+            .Struct => {
+                inline for (game_state_info.Struct.fields) |field| {
+                    switch (@typeInfo(field.type)) {
+                        .Struct => {
+                            // const inner_struct_info = @typeInfo(field.type);
+
+                            // Check if struct has `deinit` method
+                            if (@hasDecl(field.type, "deinit")) {
+                                const field_ptr = @field(self, field.name);
+                                field_ptr.deinit();
+                                std.debug.print("Deinit called on {s}\n", .{field.name});
+                            }
+                        },
+                        else => {},
+                    }
+                }
+            },
+
+            else => {},
+        }
+
+        // Loop over fields of `GameState`
+
+        self.allocator.destroy(self);
     }
 };
diff --git a/src/main.zig b/src/main.zig
index 1190d79..37ed154 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -4,8 +4,13 @@ const gs = @import("./game_state.zig");
 const assert = std.debug.assert;
 const SCREEN_WIDTH = 640;
 const SCREEN_HEIGHT = 480;
-const SCREEN_FPS: f64 = 60.0;
+const SCREEN_FPS: f64 = 30.0;
 const SCREEN_TICKS_PER_FRAME: f64 = 1000 / SCREEN_FPS;
+const PHYS_UPS: f64 = 310.0;
+const PHYS_TICKS_PER_UPDATE: f64 = 1000 / PHYS_UPS;
+const PHYS_UPDATES_PER_RENDER: f64 = PHYS_UPS / SCREEN_FPS;
+const phys_updates_per_render: u32 = @intFromFloat(PHYS_UPDATES_PER_RENDER);
+
 const log = sdl.SDL_Log;
 const GameText = @import("./text.zig").GameText;
 const RGBAColor = @import("./utils/rgb_color.zig").RGBAColor;
@@ -28,36 +33,20 @@ pub fn main() !void {
     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
     const allocator = gpa.allocator();
 
-    try GameText.initFont();
-    defer GameText.deinitFont();
-
-    var game_state = try gs.GameState.init(allocator, renderer);
-    defer game_state.deinit();
+    var game_state_controller = try gs.GameStateController.init(allocator, renderer);
+    defer game_state_controller.deinit();
 
     var quit = false;
-    var fps_timer = Timer{};
-    fps_timer.start();
+
     var frame_time = Timer{};
+    frame_time.start();
+
     var prev_frame_ms: f64 = 16.0;
     var accumulator: f64 = 0.0;
+    var prev_game_state: ?*const gs.GameState = null;
+    var current_game_state: ?*const gs.GameState = undefined;
 
     while (!quit) {
-        frame_time.reset();
-        var event: sdl.SDL_Event = undefined;
-        while (sdl.SDL_PollEvent(&event) != 0) {
-            switch (event.type) {
-                sdl.SDL_QUIT => {
-                    quit = true;
-                },
-                sdl.SDL_KEYDOWN => {
-                    switch (event.key.keysym.sym) {
-                        else => {},
-                    }
-                    log("Got key event: %i\n", event.key.keysym.sym);
-                },
-                else => {},
-            }
-        }
 
         // double newTime = time();
         // double frameTime = newTime - currentTime;
@@ -80,17 +69,60 @@ pub fn main() !void {
         // State state = currentState * alpha +
         //     previousState * ( 1.0 - alpha );
 
-        // render( state );
-        accumulator = accumulator + prev_frame_ms;
-        while (accumulator >= SCREEN_TICKS_PER_FRAME) {
-            try game_state.update_tick();
-            accumulator = accumulator - SCREEN_TICKS_PER_FRAME;
-            std.debug.print("Tick update done. Acc: {d}\n", .{accumulator});
+        // Wait for time to render screen, keep running physics
+        accumulator = accumulator + SCREEN_TICKS_PER_FRAME;
+        const total_phys_ticks: u32 = @intFromFloat(PHYS_TICKS_PER_UPDATE * PHYS_UPDATES_PER_RENDER);
+        const phys_done_at_ticks: u32 = sdl.SDL_GetTicks() + total_phys_ticks;
+        var phys_ticks_used: f64 = 0;
+        std.debug.print("phys ticks/upd: {d}, Acc: {d}\nTotal phys ticks {d}ms\nPhys done at {d}ms\n", .{ PHYS_TICKS_PER_UPDATE, accumulator, total_phys_ticks, phys_done_at_ticks });
+        // Run a physics update
+        while (accumulator >= PHYS_TICKS_PER_UPDATE) {
+            // Control loop here
+            var event: sdl.SDL_Event = undefined;
+            while (sdl.SDL_PollEvent(&event) != 0) {
+                switch (event.type) {
+                    sdl.SDL_QUIT => {
+                        quit = true;
+                    },
+                    sdl.SDL_KEYDOWN => {
+                        switch (event.key.keysym.sym) {
+                            else => {},
+                        }
+                        log("Got key event: %i\n", event.key.keysym.sym);
+                    },
+                    else => {},
+                }
+            }
+            prev_game_state = current_game_state;
+            current_game_state = try game_state_controller.update_tick();
+            const phys_ticks_this_update: f64 = current_game_state.?.phys_ticks - phys_ticks_used;
+
+            std.debug.print(" PU in {d}ms ", .{phys_ticks_this_update});
+            std.debug.print(" Acc:{d} - ", .{accumulator});
+
+            // TODO dont turn it into an int, take the extra phys_frames and do a partial render
+            if (phys_ticks_this_update < PHYS_TICKS_PER_UPDATE) {
+                const ticks: u32 = @intFromFloat(PHYS_TICKS_PER_UPDATE - phys_ticks_this_update);
+                sdl.SDL_Delay(ticks);
+                accumulator -= PHYS_TICKS_PER_UPDATE;
+            } else {
+                std.debug.print("\nPHYSICS JANK\nPhysics Update took {d}ms, target: {d}ms\n", .{ phys_ticks_this_update, PHYS_TICKS_PER_UPDATE });
+                accumulator -= phys_ticks_this_update;
+            }
+            phys_ticks_used = current_game_state.?.phys_ticks;
+            std.debug.print(" PhyTicks this render: {d} ", .{current_game_state.?.phys_ticks});
         }
 
-        game_state.render();
+        // TODO lerp together prev and current states
+        // Otherwise will have physics jank
+
+        // Update screen frame
+        std.debug.print("Took {d} phys frames\n", .{current_game_state.?.phys_updates_since_render});
+        try game_state_controller.render(current_game_state.?);
 
         prev_frame_ms = @floatFromInt(frame_time.getTicks());
+        std.debug.print("Rendered to screen in {d}ms\n***\n***\n", .{prev_frame_ms});
+        frame_time.reset();
         // const frame_time_ticks = frame_time.getTicks();
         // if (frame_time_ticks < SCREEN_TICKS_PER_FRAME) {
         //     sdl.SDL_Delay(SCREEN_TICKS_PER_FRAME - frame_time_ticks);
diff --git a/src/slime_factory.zig b/src/slime_factory.zig
index 6f2df38..759fc75 100644
--- a/src/slime_factory.zig
+++ b/src/slime_factory.zig
@@ -18,11 +18,11 @@ pub const Slime = struct {
     },
 };
 
-pub const SlimeFactory = struct {
+pub const SlimeFactoryController = struct {
     slimes: std.ArrayList(Slime) = undefined,
     slime_sprite_sheet_texture: *sdl.struct_SDL_Texture = undefined,
 
-    pub fn init(allocator: std.mem.Allocator, renderer: *sdl.struct_SDL_Renderer) !SlimeFactory {
+    pub fn init(allocator: std.mem.Allocator, renderer: *sdl.struct_SDL_Renderer) !SlimeFactoryController {
         const texture = try atils.loadTexture(renderer, slime_sprite_sheet_texture_path);
         const val = sdl.SDL_SetTextureBlendMode(texture, sdl.SDL_BLENDMODE_BLEND);
         if (val != 0) {
@@ -35,33 +35,61 @@ pub const SlimeFactory = struct {
         };
     }
 
-    pub fn deinit(sf: *SlimeFactory) void {
+    pub fn deinit(sf: *SlimeFactoryController) void {
         sdl.SDL_DestroyTexture(sf.slime_sprite_sheet_texture);
         sf.slimes.deinit();
     }
 
-    pub fn add(sf: *SlimeFactory, slime: Slime) !void {
+    pub fn add(sf: *SlimeFactoryController, slime: Slime) !void {
         try sf.slimes.append(slime);
     }
 
-    pub fn render(sf: *SlimeFactory, game_state: *GameState) void {
+    pub fn render(sf: *SlimeFactoryController, renderer: *sdl.SDL_Renderer) !void {
         if (sf.slimes.items.len == 0) return;
 
         const slime_color = sf.slimes.items[0].color;
 
-        _ = sdl.SDL_SetTextureColorMod(
+        var res = sdl.SDL_SetTextureColorMod(
             sf.slime_sprite_sheet_texture,
             slime_color.r,
             slime_color.g,
             slime_color.b,
         );
-        _ = sdl.SDL_SetTextureAlphaMod(sf.slime_sprite_sheet_texture, slime_color.a);
+        if (res > 0) {
+            sdl.SDL_Log("Error setting slime texture color: %s\n", sdl.SDL_GetError());
+            return error.FailedToRenderSlimes;
+        }
+
+        res = sdl.SDL_SetTextureAlphaMod(sf.slime_sprite_sheet_texture, slime_color.a);
+        if (res > 0) {
+            sdl.SDL_Log("Error setting slime texture alpha: %s\n", sdl.SDL_GetError());
+            return error.FailedToRenderSlimes;
+        }
         for (sf.slimes.items, 0..) |slime, s_idx| {
             const sprite_frame_x_dim: u16 = slime.f * 64;
             const sprite_frame_rect: sdl.struct_SDL_Rect = .{ .x = sprite_frame_x_dim, .y = 0, .w = 64, .h = 64 };
             const dest_rect: sdl.struct_SDL_Rect = .{ .x = slime.x, .y = slime.y, .w = 64, .h = 64 };
-            _ = sdl.SDL_RenderCopy(game_state.renderer, sf.slime_sprite_sheet_texture, &sprite_frame_rect, &dest_rect);
+            if (sdl.SDL_RenderCopy(renderer, sf.slime_sprite_sheet_texture, &sprite_frame_rect, &dest_rect) > 0) {
+                sdl.SDL_Log("Error copying slime texture to renderer: %s\n", sdl.SDL_GetError());
+                return error.FailedToRenderSlimes;
+            }
             sf.slimes.items[s_idx].f = (sf.slimes.items[s_idx].f + 1) % sf.slimes.items[s_idx].total_frames;
         }
     }
+
+    pub fn spawnFactory(sfc: *SlimeFactoryController, allocator: std.mem.Allocator) !SlimeFactory {
+        const slimes = sfc.slimes.items;
+        const slimes_copy = try allocator.alloc(Slime, slimes.len);
+        errdefer allocator.free(slimes_copy);
+
+        std.mem.copyForwards(Slime, slimes_copy, slimes);
+
+        return .{
+            .slimes = &slimes_copy,
+        };
+    }
+};
+
+pub const SlimeFactory = struct {
+    slimes: *const []Slime,
 };
diff --git a/src/text.zig b/src/text.zig
index 60b8714..106a430 100644
--- a/src/text.zig
+++ b/src/text.zig
@@ -2,38 +2,109 @@ const sdl = @import("./sdl.zig").c;
 const std = @import("std");
 const GameState = @import("./game_state.zig").GameState;
 const Offset = @import("./utils/offset.zig").Offset;
+const RGBAColor = @import("./utils/rgb_color.zig").RGBAColor;
 const FONT_SIZE = 24;
 
 var font: ?*sdl.TTF_Font = null;
-var text_texture: ?*sdl.SDL_Texture = null;
 var text_init = false;
 
+pub const GameTextFactoryController = struct {
+    texts: std.ArrayList(GameText),
+
+    pub fn init(allocator: std.mem.Allocator) !GameTextFactoryController {
+        try initFont();
+        return .{
+            .texts = std.ArrayList(GameText).init(allocator),
+        };
+    }
+
+    fn initFont() !void {
+        const loaded_font = sdl.TTF_OpenFont("./assets/fonts/DepartureMonoNF-Regular.ttf", FONT_SIZE) orelse {
+            sdl.SDL_Log("Error loading ttf font: %s\n", sdl.TTF_GetError());
+            return error.FailedToLoadFont;
+        };
+
+        font = loaded_font;
+        text_init = true;
+    }
+
+    pub fn deinit(self: *GameTextFactoryController) void {
+        self.texts.deinit();
+        deinitFont();
+    }
+
+    fn deinitFont() void {
+        sdl.TTF_CloseFont(font);
+        text_init = false;
+    }
+
+    pub fn addText(self: *GameTextFactoryController, text_ctx: GameTextContext) !void {
+        try self.texts.append(GameText.initFromContext(text_ctx));
+    }
+
+    pub fn render(self: *GameTextFactoryController, renderer: *sdl.SDL_Renderer, game_state: *const GameState) !void {
+        _ = game_state;
+        for (self.texts.items) |text| {
+            const c_text: [*c]const u8 = @ptrCast(text.text);
+            // TODO dont create new surface if unchanged
+            const text_surface: [*c]sdl.SDL_Surface = sdl.TTF_RenderText_Solid(font.?, c_text, text.color) orelse {
+                sdl.SDL_Log("Error loading text surface: %s\n", sdl.SDL_GetError());
+                return error.FailedToRenderSurface;
+            };
+            defer sdl.SDL_FreeSurface(text_surface);
+
+            if (!text_init) {
+                return error.TextNotInitialized;
+            }
+
+            // TODO dont create new texture if unchanged
+            const text_texture = sdl.SDL_CreateTextureFromSurface(renderer, text_surface) orelse {
+                sdl.SDL_Log("Error loading text texture: %s\n", sdl.SDL_GetError());
+                return error.FailedToRenderTexture;
+            };
+            defer sdl.SDL_DestroyTexture(text_texture);
+
+            const w: c_int = @intCast(text_surface.*.w);
+            const h: c_int = @intCast(text_surface.*.h);
+            const srcr = sdl.SDL_Rect{ .x = 0, .y = 0, .w = w, .h = h };
+            const destr = sdl.SDL_Rect{ .x = @intCast(text.offset.x), .y = @intCast(text.offset.y), .w = w, .h = h };
+            _ = sdl.SDL_RenderCopy(renderer, text_texture, &srcr, &destr);
+        }
+    }
+
+    pub fn spawnFactory(self: *GameTextFactoryController, allocator: std.mem.Allocator) void {
+        const texts = self.texts.items;
+        var texts_copy = try allocator.alloc(GameText, texts.len);
+        errdefer allocator.free(texts_copy);
+
+        std.mem.copyForwards(GameText, texts_copy, texts);
+
+        return .{
+            .texts = &texts_copy,
+        };
+    }
+};
+
+pub const GameTextFactory = struct {
+    texts: []GameText,
+    text_surfaces: [*c]sdl.SDL_Surface,
+};
+
+pub const GameTextContext = struct {
+    text: [*:0]const u8,
+    color: sdl.SDL_Color = RGBAColor.whiteSmoke().tosdl(),
+    // TODO Constraints??
+    offset: Offset = .{ .x = 0, .y = 0 },
+};
+
 pub const GameText = struct {
     text: [*:0]const u8,
     color: sdl.SDL_Color,
-    surface: *sdl.SDL_Surface,
-    w: u32,
-    h: u32,
+    offset: Offset,
 
-    pub fn loadFromRenderedText(text: [*:0]const u8, color: sdl.SDL_Color) !GameText {
-        const c_text: [*c]const u8 = @ptrCast(text);
-        const text_surface: [*c]sdl.SDL_Surface = sdl.TTF_RenderText_Solid(font.?, c_text, color) orelse {
-            sdl.SDL_Log("Error loading text surface: %s\n", sdl.SDL_GetError());
-            return error.FailedToRenderSurface;
-        };
-
-        return .{
-            .surface = text_surface,
-            .text = text,
-            .color = color,
-            .w = @intCast(text_surface.*.w),
-            .h = @intCast(text_surface.*.h),
-        };
-    }
-
-    pub fn deinit(self: *GameText) void {
-        sdl.SDL_FreeSurface(self.text_surface);
-    }
+    // pub fn deinit(self: *GameText) void {
+    //     sdl.SDL_FreeSurface(self.text_surface);
+    // }
 
     pub fn setBlendMode(self: *GameText, blend: sdl.SDL_BlendMode) !void {
         _ = self;
@@ -47,44 +118,11 @@ pub const GameText = struct {
         return error.Unimplemented;
     }
 
-    pub fn render(
-        self: *GameText,
-        game_state: *GameState,
-        offset: Offset,
-    ) !void {
-        if (!text_init) {
-            return error.TextNotInitialized;
-        }
-
-        if (text_texture != null) {
-            sdl.SDL_DestroyTexture(text_texture);
-            text_texture = null;
-        }
-
-        // TODO dont create new texture if unchanged
-        text_texture = sdl.SDL_CreateTextureFromSurface(game_state.renderer, self.surface) orelse {
-            sdl.SDL_Log("Error loading text texture: %s\n", sdl.SDL_GetError());
-            return error.FailedToRenderTexture;
+    pub fn initFromContext(ctx: GameTextContext) GameText {
+        return .{
+            .text = ctx.text,
+            .color = ctx.color,
+            .offset = ctx.offset,
         };
-
-        const srcr = sdl.SDL_Rect{ .x = 0, .y = 0, .w = @intCast(self.w), .h = @intCast(self.h) };
-        const destr = sdl.SDL_Rect{ .x = @intCast(offset.x), .y = @intCast(offset.y), .w = @intCast(self.w), .h = @intCast(self.h) };
-        _ = sdl.SDL_RenderCopy(game_state.renderer, text_texture, &srcr, &destr);
-    }
-
-    pub fn initFont() !void {
-        const loaded_font = sdl.TTF_OpenFont("./assets/fonts/DepartureMonoNF-Regular.ttf", FONT_SIZE) orelse {
-            sdl.SDL_Log("Error loading ttf font: %s\n", sdl.TTF_GetError());
-            return error.FailedToLoadFont;
-        };
-
-        font = loaded_font;
-        text_init = true;
-    }
-
-    pub fn deinitFont() void {
-        sdl.SDL_DestroyTexture(text_texture);
-        sdl.TTF_CloseFont(font);
-        text_init = false;
     }
 };
diff --git a/src/utils/offset.zig b/src/utils/offset.zig
index 386bc5f..6f9e808 100644
--- a/src/utils/offset.zig
+++ b/src/utils/offset.zig
@@ -1,4 +1,4 @@
 pub const Offset = struct {
-    x: i32,
-    y: i32,
+    x: i32 = 0,
+    y: i32 = 0,
 };
diff --git a/src/utils/timer.zig b/src/utils/timer.zig
index 8abe812..f1a2be6 100644
--- a/src/utils/timer.zig
+++ b/src/utils/timer.zig
@@ -41,6 +41,7 @@ pub const Timer = struct {
         self.pause_ticks_ms = 0;
     }
 
+    // Gets the current tick/ms of the timer
     pub fn getTicks(self: *Timer) u32 {
         switch (self.status) {
             .stopped => {