implement shop
This commit is contained in:
parent
365c7a0163
commit
8d5e7da6a6
@ -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
|
||||
};
|
||||
@ -199,9 +199,19 @@ pub fn spawnPickup(self: *CombatScreen) !void {
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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 => {}
|
||||
|
||||
@ -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 {
|
||||
|
||||
64
src/game.zig
64
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,25 +37,54 @@ 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 {
|
||||
@ -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;
|
||||
|
||||
181
src/shop_screen.zig
Normal file
181
src/shop_screen.zig
Normal 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
15
src/state.zig
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user