implement shop

This commit is contained in:
Rokas Puzonas 2026-01-31 17:14:28 +02:00
parent 365c7a0163
commit 8d5e7da6a6
7 changed files with 307 additions and 30 deletions

View File

@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Assets = @import("./assets.zig"); const Assets = @import("./assets.zig");
const State = @import("./state.zig");
const Engine = @import("./engine/root.zig"); const Engine = @import("./engine/root.zig");
const Nanoseconds = Engine.Nanoseconds; const Nanoseconds = Engine.Nanoseconds;
@ -40,7 +41,6 @@ const Kinetic = struct {
const Player = struct { const Player = struct {
kinetic: Kinetic = .{}, kinetic: Kinetic = .{},
money: u32 = 0,
health: u32 = 0, health: u32 = 0,
max_health: u32 = 0, max_health: u32 = 0,
last_shot_at: ?Nanoseconds = null, last_shot_at: ?Nanoseconds = null,
@ -117,7 +117,7 @@ wave_timer: Nanoseconds = 0,
waves: std.ArrayList(Wave) = .empty, waves: std.ArrayList(Wave) = .empty,
spawned_waves: std.ArrayList(usize) = .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); var rng = RNGState.init(seed);
const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(rng.random()); 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, .next_pickup_spawn_at = next_pickup_spawn_at,
.player = .{ .player = .{
.kinetic = .{ .kinetic = .{
.pos = .init(50, 50), .pos = world_size.divideScalar(2)
}, },
.health = 3, .health = state.max_health,
.max_health = 3 .max_health = state.max_health,
}, },
.rng = rng .rng = rng
}; };
@ -197,11 +197,21 @@ pub fn spawnPickup(self: *CombatScreen) !void {
.pos = pos, .pos = pos,
.kind = .money .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(); const dt = frame.deltaTime();
var result = TickResult{
.player_died = false,
.player_finished = false,
};
self.wave_timer += frame.dt_ns; self.wave_timer += frame.dt_ns;
const wave_timer_s = @divFloor(self.wave_timer, std.time.ns_per_s); 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())) { if (pickup_rect.hasOverlap(self.player.getRect())) {
switch (pickup.kind) { switch (pickup.kind) {
.money => { .money => {
self.player.money += 1; state.money += 1;
} }
} }
destroy = true; destroy = true;
@ -432,8 +442,15 @@ pub fn tick(self: *CombatScreen, frame: *Engine.Frame) !void {
@mod(wave_timer_s, 60) @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 }); 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 { fn getCenteredRect(pos: Vec2, size: f32) Rect {

View File

@ -30,11 +30,13 @@ pub const Input = struct {
keyboard: InputSystem.ButtonStateSet(KeyCode), keyboard: InputSystem.ButtonStateSet(KeyCode),
mouse_button: InputSystem.ButtonStateSet(MouseButton), mouse_button: InputSystem.ButtonStateSet(MouseButton),
mouse_position: ?Vec2, mouse_position: ?Vec2,
mouse_delta: Vec2,
pub const empty = Input{ pub const empty = Input{
.keyboard = .empty, .keyboard = .empty,
.mouse_button = .empty, .mouse_button = .empty,
.mouse_position = null, .mouse_position = null,
.mouse_delta = .zero,
}; };
}; };

View File

@ -87,6 +87,8 @@ pub const Event = union(enum) {
char: u21, char: u21,
}; };
pub var mouse_position: ?Vec2 = null;
pub fn processEvent(frame: *Frame, event: Event) void { pub fn processEvent(frame: *Frame, event: Event) void {
const input = &frame.input; const input = &frame.input;
@ -102,21 +104,21 @@ pub fn processEvent(frame: *Frame, event: Event) void {
.mouse_leave => { .mouse_leave => {
input.keyboard.releaseAll(); input.keyboard.releaseAll();
input.mouse_position = null; mouse_position = null;
input.mouse_button = .empty; input.mouse_button = .empty;
}, },
.mouse_enter => |pos| { .mouse_enter => |pos| {
input.mouse_position = pos; mouse_position = pos;
}, },
.mouse_move => |pos| { .mouse_move => |pos| {
input.mouse_position = pos; mouse_position = pos;
}, },
.mouse_pressed => |opts| { .mouse_pressed => |opts| {
input.mouse_position = opts.position; mouse_position = opts.position;
input.mouse_button.press(opts.button, frame.time_ns); input.mouse_button.press(opts.button, frame.time_ns);
}, },
.mouse_released => |opts| { .mouse_released => |opts| {
input.mouse_position = opts.position; mouse_position = opts.position;
input.mouse_button.release(opts.button); input.mouse_button.release(opts.button);
}, },
else => {} else => {}

View File

@ -167,16 +167,6 @@ fn sokolFrame(self: *Engine) !void {
const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); 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); _ = frame.arena.reset(.retain_capacity);
const arena = frame.arena.allocator(); const arena = frame.arena.allocator();
@ -197,6 +187,20 @@ fn sokolFrame(self: *Engine) !void {
frame.dt_ns = time_passed - self.last_frame_at; frame.dt_ns = time_passed - self.last_frame_at;
frame.hide_cursor = false; 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); try self.game.tick(&self.frame);
frame.input.keyboard.pressed = .initEmpty(); frame.input.keyboard.pressed = .initEmpty();
@ -238,10 +242,6 @@ fn sokolFrame(self: *Engine) !void {
for (frame.audio.commands.items) |command| { for (frame.audio.commands.items) |command| {
try Audio.mixer.commands.push(command); try Audio.mixer.commands.push(command);
} }
if (revert_mouse_position) |pos| {
self.frame.input.mouse_position = pos;
}
} }
fn showDebugWindow(frame: *Frame) !void { fn showDebugWindow(frame: *Frame) !void {

View File

@ -25,7 +25,9 @@ const Sprite = Engine.Graphics.Sprite;
const RaycastTileIterator = @import("./raycast_tile_iterator.zig"); const RaycastTileIterator = @import("./raycast_tile_iterator.zig");
const State = @import("./state.zig");
const CombatScreen = @import("./combat_screen.zig"); const CombatScreen = @import("./combat_screen.zig");
const ShopScreen = @import("./shop_screen.zig");
const Game = @This(); const Game = @This();
const world_size = Vec2.init(20 * 16, 15 * 16); const world_size = Vec2.init(20 * 16, 15 * 16);
@ -35,27 +37,56 @@ const RNGState = std.Random.DefaultPrng;
arena: std.heap.ArenaAllocator, arena: std.heap.ArenaAllocator,
gpa: Allocator, gpa: Allocator,
assets: *Assets, assets: *Assets,
rng: RNGState,
state: State,
combat_screen: CombatScreen, combat_screen: CombatScreen,
shop_screen: ShopScreen,
show_shop: bool = false,
pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game { pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game {
var arena = std.heap.ArenaAllocator.init(gpa); var arena = std.heap.ArenaAllocator.init(gpa);
errdefer arena.deinit(); 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{ return Game{
.arena = arena, .arena = arena,
.gpa = gpa, .gpa = gpa,
.assets = assets, .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 { pub fn deinit(self: *Game) void {
self.arena.deinit(); self.arena.deinit();
self.state.deinit();
self.combat_screen.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 { pub fn tick(self: *Game, frame: *Engine.Frame) !void {
if (frame.isKeyPressed(.ESCAPE)) { if (frame.isKeyPressed(.ESCAPE)) {
sapp.requestQuit(); sapp.requestQuit();
@ -65,7 +96,22 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void {
frame.show_debug = !frame.show_debug; 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 { pub fn debug(self: *Game) !void {
@ -82,6 +128,20 @@ pub fn debug(self: *Game) !void {
try self.combat_screen.spawnEnemy(); 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 screen = &self.combat_screen;
const time_left_til_pickup = screen.next_pickup_spawn_at - screen.wave_timer; const time_left_til_pickup = screen.next_pickup_spawn_at - screen.wave_timer;

181
src/shop_screen.zig Normal file
View File

@ -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;
}

15
src/state.zig Normal file
View File

@ -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
}