From 8d5e7da6a6c8d18f167c0e0b4418bf64766ff219 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 31 Jan 2026 17:14:28 +0200 Subject: [PATCH] implement shop --- src/combat_screen.zig | 35 +++++--- src/engine/frame.zig | 2 + src/engine/input.zig | 12 +-- src/engine/root.zig | 28 +++---- src/game.zig | 64 ++++++++++++++- src/shop_screen.zig | 181 ++++++++++++++++++++++++++++++++++++++++++ src/state.zig | 15 ++++ 7 files changed, 307 insertions(+), 30 deletions(-) create mode 100644 src/shop_screen.zig create mode 100644 src/state.zig diff --git a/src/combat_screen.zig b/src/combat_screen.zig index 79d70b2..d2fcffa 100644 --- a/src/combat_screen.zig +++ b/src/combat_screen.zig @@ -2,6 +2,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Assets = @import("./assets.zig"); +const State = @import("./state.zig"); const Engine = @import("./engine/root.zig"); const Nanoseconds = Engine.Nanoseconds; @@ -40,7 +41,6 @@ const Kinetic = struct { const Player = struct { kinetic: Kinetic = .{}, - money: u32 = 0, health: u32 = 0, max_health: u32 = 0, last_shot_at: ?Nanoseconds = null, @@ -117,7 +117,7 @@ wave_timer: Nanoseconds = 0, waves: std.ArrayList(Wave) = .empty, spawned_waves: std.ArrayList(usize) = .empty, -pub fn init(gpa: Allocator, seed: u64, assets: *Assets) CombatScreen { +pub fn init(gpa: Allocator, seed: u64, assets: *Assets, state: State) CombatScreen { var rng = RNGState.init(seed); const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(rng.random()); @@ -129,10 +129,10 @@ pub fn init(gpa: Allocator, seed: u64, assets: *Assets) CombatScreen { .next_pickup_spawn_at = next_pickup_spawn_at, .player = .{ .kinetic = .{ - .pos = .init(50, 50), + .pos = world_size.divideScalar(2) }, - .health = 3, - .max_health = 3 + .health = state.max_health, + .max_health = state.max_health, }, .rng = rng }; @@ -197,11 +197,21 @@ pub fn spawnPickup(self: *CombatScreen) !void { .pos = pos, .kind = .money }); -} +} -pub fn tick(self: *CombatScreen, frame: *Engine.Frame) !void { +const TickResult = struct { + player_died: bool, + player_finished: bool +}; + +pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResult { const dt = frame.deltaTime(); + var result = TickResult{ + .player_died = false, + .player_finished = false, + }; + self.wave_timer += frame.dt_ns; const wave_timer_s = @divFloor(self.wave_timer, std.time.ns_per_s); @@ -383,7 +393,7 @@ pub fn tick(self: *CombatScreen, frame: *Engine.Frame) !void { if (pickup_rect.hasOverlap(self.player.getRect())) { switch (pickup.kind) { .money => { - self.player.money += 1; + state.money += 1; } } destroy = true; @@ -432,8 +442,15 @@ pub fn tick(self: *CombatScreen, frame: *Engine.Frame) !void { @mod(wave_timer_s, 60) }); - frame.drawTextFormat(.init(10, 30), text_opts, "{d}", .{ self.player.money }); + frame.drawTextFormat(.init(10, 30), text_opts, "{d}", .{ state.money }); frame.drawTextFormat(.init(10, 50), text_opts, "{d}/{d}", .{ self.player.health, self.player.max_health }); + + result.player_died = (self.player.health == 0); + if (self.enemies.items.len == 0 and self.waves.items.len == 0 and self.spawned_waves.items.len == wave_infos.len) { + result.player_finished = true; + } + + return result; } fn getCenteredRect(pos: Vec2, size: f32) Rect { diff --git a/src/engine/frame.zig b/src/engine/frame.zig index fc800cf..42b057d 100644 --- a/src/engine/frame.zig +++ b/src/engine/frame.zig @@ -30,11 +30,13 @@ 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, }; }; diff --git a/src/engine/input.zig b/src/engine/input.zig index ee7ed33..a40a4f6 100644 --- a/src/engine/input.zig +++ b/src/engine/input.zig @@ -87,6 +87,8 @@ pub const Event = union(enum) { char: u21, }; +pub var mouse_position: ?Vec2 = null; + pub fn processEvent(frame: *Frame, event: Event) void { const input = &frame.input; @@ -102,21 +104,21 @@ pub fn processEvent(frame: *Frame, event: Event) void { .mouse_leave => { input.keyboard.releaseAll(); - input.mouse_position = null; + mouse_position = null; input.mouse_button = .empty; }, .mouse_enter => |pos| { - input.mouse_position = pos; + mouse_position = pos; }, .mouse_move => |pos| { - input.mouse_position = pos; + mouse_position = pos; }, .mouse_pressed => |opts| { - input.mouse_position = opts.position; + mouse_position = opts.position; input.mouse_button.press(opts.button, frame.time_ns); }, .mouse_released => |opts| { - input.mouse_position = opts.position; + mouse_position = opts.position; input.mouse_button.release(opts.button); }, else => {} diff --git a/src/engine/root.zig b/src/engine/root.zig index 3ae7a79..970a18e 100644 --- a/src/engine/root.zig +++ b/src/engine/root.zig @@ -167,16 +167,6 @@ fn sokolFrame(self: *Engine) !void { const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); - var revert_mouse_position: ?Vec2 = null; - if (self.canvas_size) |canvas_size| { - if (self.frame.input.mouse_position) |mouse| { - const transform = ScreenScalar.init(screen_size, canvas_size); - - revert_mouse_position = mouse; - self.frame.input.mouse_position = mouse.sub(transform.translation).divideScalar(transform.scale); - } - } - { _ = frame.arena.reset(.retain_capacity); const arena = frame.arena.allocator(); @@ -197,6 +187,20 @@ fn sokolFrame(self: *Engine) !void { frame.dt_ns = time_passed - self.last_frame_at; frame.hide_cursor = false; + var mouse_position = Input.mouse_position; + if (self.canvas_size) |canvas_size| { + if (mouse_position) |mouse| { + const transform = ScreenScalar.init(screen_size, canvas_size); + + mouse_position = mouse.sub(transform.translation).divideScalar(transform.scale); + } + } + frame.input.mouse_delta = .zero; + if (mouse_position != null and frame.input.mouse_position != null) { + frame.input.mouse_delta = frame.input.mouse_position.?.sub(mouse_position.?); + } + frame.input.mouse_position = mouse_position; + try self.game.tick(&self.frame); frame.input.keyboard.pressed = .initEmpty(); @@ -238,10 +242,6 @@ fn sokolFrame(self: *Engine) !void { for (frame.audio.commands.items) |command| { try Audio.mixer.commands.push(command); } - - if (revert_mouse_position) |pos| { - self.frame.input.mouse_position = pos; - } } fn showDebugWindow(frame: *Frame) !void { diff --git a/src/game.zig b/src/game.zig index ac4d182..f65e393 100644 --- a/src/game.zig +++ b/src/game.zig @@ -25,7 +25,9 @@ const Sprite = Engine.Graphics.Sprite; const RaycastTileIterator = @import("./raycast_tile_iterator.zig"); +const State = @import("./state.zig"); const CombatScreen = @import("./combat_screen.zig"); +const ShopScreen = @import("./shop_screen.zig"); const Game = @This(); const world_size = Vec2.init(20 * 16, 15 * 16); @@ -35,27 +37,56 @@ const RNGState = std.Random.DefaultPrng; arena: std.heap.ArenaAllocator, gpa: Allocator, assets: *Assets, +rng: RNGState, + +state: State, combat_screen: CombatScreen, +shop_screen: ShopScreen, + +show_shop: bool = false, pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game { var arena = std.heap.ArenaAllocator.init(gpa); errdefer arena.deinit(); + var rng = RNGState.init(seed); + + var state = State.init(); + errdefer state.deinit(); + + var combat_screen = CombatScreen.init(gpa, rng.next(), assets, state); + errdefer combat_screen.deinit(); + + var shop_screen = try ShopScreen.init(gpa, assets); + errdefer shop_screen.deinit(); + return Game{ .arena = arena, .gpa = gpa, .assets = assets, + .rng = rng, - .combat_screen = .init(gpa, seed, assets), + .state = state, + + .combat_screen = combat_screen, + .shop_screen = shop_screen, }; } pub fn deinit(self: *Game) void { self.arena.deinit(); + self.state.deinit(); self.combat_screen.deinit(); + self.shop_screen.deinit(); } +pub fn restartAndShowCombatScreen(self: *Game) !void { + self.combat_screen.deinit(); + self.combat_screen = .init(self.gpa, self.rng.next(), self.assets, self.state); + self.show_shop = false; +} + pub fn tick(self: *Game, frame: *Engine.Frame) !void { if (frame.isKeyPressed(.ESCAPE)) { sapp.requestQuit(); @@ -65,7 +96,22 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { frame.show_debug = !frame.show_debug; } - try self.combat_screen.tick(frame); + if (frame.isKeyPressed(.F2)) { + self.show_shop = !self.show_shop; + } + + if (self.show_shop) { + const result = try self.shop_screen.tick(&self.state, frame); + if (result.back_to_combat) { + try self.restartAndShowCombatScreen(); + } + } else { + const result = try self.combat_screen.tick(&self.state, frame); + + if (result.player_finished or result.player_died) { + self.show_shop = true; + } + } } pub fn debug(self: *Game) !void { @@ -82,6 +128,20 @@ pub fn debug(self: *Game) !void { try self.combat_screen.spawnEnemy(); } + if (imgui.button("Restart")) { + try self.restartAndShowCombatScreen(); + } + + if (self.show_shop) { + if (imgui.button("Swap to combat")) { + self.show_shop = false; + } + } else { + if (imgui.button("Swap to shop")) { + self.show_shop = true; + } + } + const screen = &self.combat_screen; const time_left_til_pickup = screen.next_pickup_spawn_at - screen.wave_timer; diff --git a/src/shop_screen.zig b/src/shop_screen.zig new file mode 100644 index 0000000..08aef73 --- /dev/null +++ b/src/shop_screen.zig @@ -0,0 +1,181 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const State = @import("./state.zig"); +const Assets = @import("./assets.zig"); + +const Engine = @import("./engine/root.zig"); +const Frame = Engine.Frame; +const Vec2 = Engine.Vec2; +const Vec4 = Engine.Math.Vec4; +const rgb = Engine.Math.rgb; +const Rect = Engine.Math.Rect; + +const ShopScreen = @This(); + +const canvas_size = Vec2.init(20 * 16, 15 * 16); +const canvas_bounds = Rect.init(-200, -200, 400, 400); +const node_size = Vec2.init(10, 10); + +const Upgrade = union(enum) { + unlock_pistol, + increase_health, + + pub fn apply(self: Upgrade, state: *State) void { + switch (self) { + .unlock_pistol => { + state.has_pistol_unlocked = true; + }, + .increase_health => { + const max_health: f32 = @floatFromInt(state.max_health); + state.max_health = @intFromFloat(max_health * 1.5); + } + } + } +}; + +const UpgradeNode = struct { + pos: Vec2, + money_cost: u32, + upgrade: Upgrade, + dependency: ?usize = null, + bought: bool = false +}; + +gpa: Allocator, +assets: *Assets, +nodes: std.ArrayList(UpgradeNode) = .empty, +camera_pos: Vec2 = .zero, + +pub fn init(gpa: Allocator, assets: *Assets) !ShopScreen { + var nodes: std.ArrayList(UpgradeNode) = .empty; + errdefer nodes.deinit(gpa); + + try nodes.append(gpa, UpgradeNode{ + .pos = .init(0, 0), + .upgrade = .unlock_pistol, + .money_cost = 1 + }); + + try nodes.append(gpa, UpgradeNode{ + .pos = .init(20, 0), + .upgrade = .increase_health, + .money_cost = 10, + .dependency = 0 + }); + + return ShopScreen{ + .gpa = gpa, + .nodes = nodes, + .assets = assets + }; +} + +pub fn deinit(self: *ShopScreen) void { + self.nodes.deinit(self.gpa); +} + +const TickResult = struct { + back_to_combat: bool +}; + +pub fn tick(self: *ShopScreen, state: *State, frame: *Frame) !TickResult { + frame.graphics.canvas_size = canvas_size; + + var result = TickResult{ + .back_to_combat = false + }; + + const camera_offset = self.camera_pos.add(canvas_size.divideScalar(2)); + var mouse: ?Vec2 = null; + if (frame.input.mouse_position) |mouse_position| { + mouse = mouse_position.sub(camera_offset); + } + + frame.drawRectangle(.{ + .rect = .init(0, 0, canvas_size.x, canvas_size.y), + .color = rgb(20, 20, 20) + }); + + { + frame.pushTransform(camera_offset, .init(1, 1)); + defer frame.popTransform(); + + frame.drawRectanglOutline( + canvas_bounds.pos, + canvas_bounds.size, + rgb(255, 255, 255), + 1 + ); + + if (frame.isMouseDown(.left)) { + self.camera_pos = self.camera_pos.sub(frame.input.mouse_delta); + + self.camera_pos.x = std.math.clamp( + self.camera_pos.x - canvas_size.x/2, + canvas_bounds.left(), + canvas_bounds.right() - canvas_size.x, + ) + canvas_size.x/2; + + self.camera_pos.y = std.math.clamp( + self.camera_pos.y - canvas_size.y/2, + canvas_bounds.top(), + canvas_bounds.bottom() - canvas_size.y, + ) + canvas_size.y/2; + } + + for (self.nodes.items) |node| { + if (node.dependency) |dependency_index| { + const dependency = self.nodes.items[dependency_index]; + frame.drawLine(node.pos, dependency.pos, rgb(200, 200, 200), 1); + } + } + + for (self.nodes.items) |*node| { + const node_rect = Rect.initCentered(node.pos.x, node.pos.y, node_size.x, node_size.y); + + const is_mouse_inside = mouse != null and node_rect.isInside(mouse.?); + const has_enough_money = state.money >= node.money_cost; + + if (has_enough_money and is_mouse_inside and frame.isMousePressed(.left)) { + node.upgrade.apply(state); + node.bought = true; + } + + var color: Vec4 = undefined; + if (node.bought) { + color = rgb(255, 255, 255); + } else if (is_mouse_inside) { + if (has_enough_money) { + color = rgb(200, 200, 20); + } else { + color = rgb(200, 20, 20); + } + } else { + color = rgb(200, 200, 200); + } + + frame.drawRectangle(.{ + .rect = node_rect, + .color = color + }); + } + } + + const text_opts = Engine.Frame.DrawTextOptions{ + .font = self.assets.font_id.get(.regular) + }; + frame.drawTextFormat(.init(10, 30), text_opts, "Money: {d}", .{ state.money }); + + const back_rect = Rect.init(10, 10, 100, 15); + frame.drawRectangle(.{ + .rect = back_rect, + .color = rgb(255, 255, 255) + }); + + if (frame.input.mouse_position != null and back_rect.isInside(frame.input.mouse_position.?) and frame.isMousePressed(.left)) { + result.back_to_combat = true; + } + + return result; +} diff --git a/src/state.zig b/src/state.zig new file mode 100644 index 0000000..1db1279 --- /dev/null +++ b/src/state.zig @@ -0,0 +1,15 @@ +const State = @This(); + +money: u32 = 0, + +max_health: u32 = 3, +has_pistol_unlocked: bool = false, + +pub fn init() State { + return State{ + }; +} + +pub fn deinit(self: *State) void { + _ = self; // autofix +}