add upgrades

This commit is contained in:
Rokas Puzonas 2026-07-17 00:24:58 +03:00
parent 46ca2e5e1e
commit 300c8c47e2
3 changed files with 302 additions and 69 deletions

View File

@ -36,6 +36,8 @@ entities_buffer: [max_entities]EntitySlotMap.Slot,
entities: EntitySlotMap, entities: EntitySlotMap,
eternal_flame_id: EntityId, eternal_flame_id: EntityId,
upgrades: Upgrades,
font: Gfx.Font.Id, font: Gfx.Font.Id,
idle_animations: AnimationDirections, idle_animations: AnimationDirections,
walk_animations: AnimationDirections, walk_animations: AnimationDirections,
@ -160,7 +162,6 @@ const Entity = struct {
minion_state: MinionState = .wander, minion_state: MinionState = .wander,
minion_chop_cooldown: Nanoseconds = 0, minion_chop_cooldown: Nanoseconds = 0,
minion_chop_damage: u32 = 2,
minion_throw_cooldown: Nanoseconds = 0, minion_throw_cooldown: Nanoseconds = 0,
minion_carried_wood: ?EntityId = null, minion_carried_wood: ?EntityId = null,
@ -178,9 +179,6 @@ const Entity = struct {
pos: Vec2 = .init(0, 0), pos: Vec2 = .init(0, 0),
vel: Vec2 = .init(0, 0), vel: Vec2 = .init(0, 0),
acc: Vec2 = .init(0, 0), acc: Vec2 = .init(0, 0),
max_velocity: f32 = 0,
friction: f32 = 0,
max_walk_speed: f32 = 0,
animation: AnimationName = .idle, animation: AnimationName = .idle,
animation_dir: AnimationDirection = .down, animation_dir: AnimationDirection = .down,
@ -215,6 +213,28 @@ const Entity = struct {
}; };
}; };
const Upgrades = struct {
minion_chop_damage: u32 = 2,
minion_chop_rate: f32 = 1,
minion_throw_cooldown: Nanoseconds = std.time.ns_per_s,
minion_walk: MinionWalkStats = .{},
tree_health: u32 = 10,
tree_respawn_duration: Nanoseconds = std.time.ns_per_s,
tree_wood_amount: u32 = 1,
burn_max_stack_size: u32 = 10,
burn_speed: f32 = 10,
burn_wood_health: f32 = 20,
burn_heat_per_wood: u32 = 1,
const MinionWalkStats = struct {
friction: f32 = 0.998,
max_velocity: f32 = 50,
max_speed: f32 = 300,
};
};
fn initImageDataFromSTB(img: STBImage) Gfx.ImageData { fn initImageDataFromSTB(img: STBImage) Gfx.ImageData {
return Gfx.ImageData{ return Gfx.ImageData{
.width = img.width, .width = img.width,
@ -272,6 +292,7 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.camera_pos = .init(0, 0), .camera_pos = .init(0, 0),
.font = font, .font = font,
.eternal_flame_id = undefined, .eternal_flame_id = undefined,
.upgrades = .{}
}; };
self.spawnMinion(.init(0, 0)); self.spawnMinion(.init(0, 0));
@ -306,9 +327,6 @@ pub fn spawnMinion(self: *App, pos: Vec2) void {
}, },
.pos = pos, .pos = pos,
.friction = 0.998,
.max_velocity = 50,
.max_walk_speed = 300,
.animation = .idle, .animation = .idle,
}); });
@ -337,7 +355,7 @@ fn spawnTree(self: *App, station_id: EntityId) void {
return; return;
} }
const health = 10; const health = self.upgrades.tree_health;
if (self.spawnEntity(Entity{ if (self.spawnEntity(Entity{
.type = .tree, .type = .tree,
.tree_health = health, .tree_health = health,
@ -353,13 +371,15 @@ fn destroyTree(self: *App, tree_id: EntityId) void {
const tree = self.entities.get(tree_id) orelse return; const tree = self.entities.get(tree_id) orelse return;
assert(tree.type == .tree); assert(tree.type == .tree);
self.spawnWood(tree.pos); for (0..self.upgrades.tree_wood_amount) |_| {
self.spawnWood(tree.pos);
}
self.entities.removeAssumeExists(tree_id); self.entities.removeAssumeExists(tree_id);
} }
fn spawnWood(self: *App, pos: Vec2) void { fn spawnWood(self: *App, pos: Vec2) void {
const health = 20; const health = self.upgrades.burn_wood_health;
_ = self.spawnEntity(Entity{ _ = self.spawnEntity(Entity{
.type = .wood, .type = .wood,
@ -522,6 +542,20 @@ fn drawProgressBar(progress: f32, center: Vec2, size: Vec2, color: Color) void {
Gfx.drawLine(from, from.add(.init(size.x * progress, 0)), color, size.y); Gfx.drawLine(from, from.add(.init(size.x * progress, 0)), color, size.y);
} }
fn countEternalFlameStackSize(self: *App) u32 {
var result: u32 = 0;
var wood_iter = self.initEntityIteratorByType(.wood);
while (wood_iter.next()) |wood_id| {
const wood = self.entities.getAssumeExists(wood_id);
if (wood.wood_burning) {
result += 1;
}
}
return result;
}
fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void {
const minion = self.entities.getAssumeExists(minion_id); const minion = self.entities.getAssumeExists(minion_id);
assert(minion.type == .minion); assert(minion.type == .minion);
@ -549,11 +583,16 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void {
if (minion.pos.distance(tree.pos) < goal_distance) { if (minion.pos.distance(tree.pos) < goal_distance) {
if (minion.minion_chop_cooldown == 0) { if (minion.minion_chop_cooldown == 0) {
minion.minion_chop_cooldown = std.time.ns_per_ms * 500; minion.minion_chop_cooldown = @intFromFloat(std.time.ns_per_s / self.upgrades.minion_chop_rate);
tree.tree_health -= minion.minion_chop_damage;
if (tree.tree_health >= self.upgrades.minion_chop_damage) {
tree.tree_health -= self.upgrades.minion_chop_damage;
} else {
tree.tree_health = 0;
}
} }
if (tree.tree_health <= 0) { if (tree.tree_health == 0) {
self.unsetEntityTarget(minion_id); self.unsetEntityTarget(minion_id);
self.destroyTree(tree_id); self.destroyTree(tree_id);
minion.minion_state = .wander; minion.minion_state = .wander;
@ -601,12 +640,13 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void {
if (minion.minion_carried_wood) |wood_id| { if (minion.minion_carried_wood) |wood_id| {
const wood = self.entities.getAssumeExists(wood_id); const wood = self.entities.getAssumeExists(wood_id);
const eternal_flame = self.entities.getAssumeExists(self.eternal_flame_id); const eternal_flame = self.entities.getAssumeExists(self.eternal_flame_id);
if (self.countEternalFlameStackSize() < self.upgrades.burn_max_stack_size) {
wood.pos = eternal_flame.pos; wood.pos = eternal_flame.pos;
wood.wood_burning = true; wood.wood_burning = true;
wood.wood_carried_by = null; wood.wood_carried_by = null;
minion.minion_carried_wood = null; minion.minion_carried_wood = null;
minion.minion_throw_cooldown = std.time.ns_per_s; minion.minion_throw_cooldown = self.upgrades.minion_throw_cooldown;
}
} else { } else {
minion.minion_throw_cooldown = @max(minion.minion_throw_cooldown - plt.dt, 0); minion.minion_throw_cooldown = @max(minion.minion_throw_cooldown - plt.dt, 0);
if (minion.minion_throw_cooldown == 0) { if (minion.minion_throw_cooldown == 0) {
@ -617,11 +657,12 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void {
} }
if (walk_goal) |pos| { if (walk_goal) |pos| {
const stats = self.upgrades.minion_walk;
const diff = minion.pos.x - pos.x; const diff = minion.pos.x - pos.x;
if (diff < -goal_distance) { if (diff < -goal_distance) {
minion.acc = .init(minion.max_walk_speed, 0); minion.acc = .init(stats.max_speed, 0);
} else if (diff > goal_distance) { } else if (diff > goal_distance) {
minion.acc = .init(-minion.max_walk_speed, 0); minion.acc = .init(-stats.max_speed, 0);
} }
} }
} }
@ -674,7 +715,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
if (wood_station.station_owned_tree) |station_owned_tree| { if (wood_station.station_owned_tree) |station_owned_tree| {
if (!self.entities.exists(station_owned_tree)) { if (!self.entities.exists(station_owned_tree)) {
wood_station.station_owned_tree = null; wood_station.station_owned_tree = null;
wood_station.station_spawn_tree_cooldown = std.time.ns_per_s; wood_station.station_spawn_tree_cooldown = self.upgrades.tree_respawn_duration;
} }
} }
@ -710,8 +751,6 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
} }
{ {
const burn_speed = 10;
var wood_iter = self.initEntityIteratorByType(.wood); var wood_iter = self.initEntityIteratorByType(.wood);
while (wood_iter.next()) |wood_id| { while (wood_iter.next()) |wood_id| {
const wood = self.entities.getAssumeExists(wood_id); const wood = self.entities.getAssumeExists(wood_id);
@ -720,10 +759,10 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
// Apply burn // Apply burn
if (wood.wood_burning) { if (wood.wood_burning) {
wood.wood_health = @max(wood.wood_health - burn_speed * dt, 0); wood.wood_health = @max(wood.wood_health - self.upgrades.burn_speed * dt, 0);
if (wood.wood_health == 0) { if (wood.wood_health == 0) {
self.heat += 1; self.heat += self.upgrades.burn_heat_per_wood;
self.entities.removeAssumeExists(wood_id); self.entities.removeAssumeExists(wood_id);
} }
@ -881,15 +920,21 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
if (e.flags.has_kinematics) { if (e.flags.has_kinematics) {
e.vel = e.vel.add(e.acc.multiplyScalar(dt)); e.vel = e.vel.add(e.acc.multiplyScalar(dt));
e.vel = e.vel.limitLength(e.max_velocity);
e.pos = e.pos.add(e.vel.multiplyScalar(dt)); e.pos = e.pos.add(e.vel.multiplyScalar(dt));
const friction_force = std.math.pow(f32, 1 - e.friction, dt);
e.vel = e.vel.multiplyScalar(friction_force);
e.acc = .init(0, 0); e.acc = .init(0, 0);
} }
if (e.type == .minion) {
const stats = self.upgrades.minion_walk;
e.vel = e.vel.limitLength(stats.max_velocity);
if (0 <= stats.friction and stats.friction <= 1) {
const friction_force = std.math.pow(f32, 1 - stats.friction, dt);
e.vel = e.vel.multiplyScalar(friction_force);
}
}
if (e.flags.has_animation) { if (e.flags.has_animation) {
const animations = switch (e.animation) { const animations = switch (e.animation) {
.idle => self.idle_animations, .idle => self.idle_animations,
@ -958,6 +1003,11 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
const eternal_flame = self.entities.getAssumeExists(id); const eternal_flame = self.entities.getAssumeExists(id);
Gfx.drawRectangle(eternal_flame.pos.sub(.init(10, 50)), .init(30, 50), .rgb(150, 30, 20)); Gfx.drawRectangle(eternal_flame.pos.sub(.init(10, 50)), .init(30, 50), .rgb(150, 30, 20));
Gfx.drawText(self.font, .{
.text = try std.fmt.allocPrint(plt.arena, "Stack: {}", .{self.countEternalFlameStackSize()}),
.pos = eternal_flame.pos.sub(.init(10, 80)),
.height = 24
});
} }
} }
@ -976,12 +1026,58 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
.text = try std.fmt.allocPrint(plt.arena, "Heat: {}", .{self.heat}), .text = try std.fmt.allocPrint(plt.arena, "Heat: {}", .{self.heat}),
}); });
if (ImGUI.beginWindow(.{ .name = "Debug", .size = .init(200, 100) })) { if (ImGUI.beginWindow(.{ .name = "Debug", .alpha = 0.8, .size = .init(500, 200) })) {
defer ImGUI.endWindow(); defer ImGUI.endWindow();
if (ImGUI.button("spawn")) { if (ImGUI.button("spawn minion")) {
self.spawnMinion(.init(32, 0)); self.spawnMinion(.init(32, 0));
} }
var heat: u32 = @intCast(self.heat);
if (ImGUI.inputU32("heat", &heat)) {
self.heat = @intCast(heat);
}
ImGUI.separator();
_ = ImGUI.text("Minion upgrades", .{});
_ = ImGUI.inputU32("chop_damage", &self.upgrades.minion_chop_damage);
_ = ImGUI.inputF32Drag(.{
.label = "chop_rate",
.value = &self.upgrades.minion_chop_rate
});
_ = ImGUI.inputDuration("throw_cooldown", &self.upgrades.minion_throw_cooldown);
_ = ImGUI.inputF32Drag(.{
.label = "walk_max_speed",
.value = &self.upgrades.minion_walk.max_speed
});
_ = ImGUI.inputF32Drag(.{
.label = "walk_max_vel",
.value = &self.upgrades.minion_walk.max_velocity
});
_ = ImGUI.inputF32Drag(.{
.label = "walk_friction",
.value = &self.upgrades.minion_walk.friction,
.ex = .{ .min = 0, .max = 1, .speed = 0.0001 }
});
ImGUI.separator();
_ = ImGUI.text("Tree upgrades", .{});
_ = ImGUI.inputU32("health", &self.upgrades.tree_health);
_ = ImGUI.inputU32("wood_amount", &self.upgrades.tree_wood_amount);
_ = ImGUI.inputDuration("respawn_duration", &self.upgrades.tree_respawn_duration);
ImGUI.separator();
_ = ImGUI.text("Burn upgrades", .{});
_ = ImGUI.inputF32Drag(.{
.label = "speed",
.value = &self.upgrades.burn_speed
});
_ = ImGUI.inputF32(.{
.label = "wood_health",
.value = &self.upgrades.burn_wood_health
});
_ = ImGUI.inputU32("heat_per_wood", &self.upgrades.burn_heat_per_wood);
_ = ImGUI.inputU32("max_stack_size", &self.upgrades.burn_max_stack_size);
} }
} }

View File

@ -15,6 +15,8 @@ const simgui = sokol.imgui;
const ig = @import("cimgui"); const ig = @import("cimgui");
const Nanoseconds = @import("./platform.zig").Nanoseconds;
const ImGUI = @This(); const ImGUI = @This();
const State = struct { const State = struct {
@ -150,6 +152,7 @@ pub const WindowOptions = struct {
size: ?Vec2 = null, size: ?Vec2 = null,
collapsed: ?bool = null, collapsed: ?bool = null,
open: ?*bool = null, open: ?*bool = null,
alpha: f32 = 1
}; };
pub fn beginWindow(opts: WindowOptions) bool { pub fn beginWindow(opts: WindowOptions) bool {
@ -175,7 +178,7 @@ pub fn beginWindow(opts: WindowOptions) bool {
ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once); ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once);
} }
ig.igSetNextWindowBgAlpha(1); ig.igSetNextWindowBgAlpha(opts.alpha);
const frame = g_state.frame.?; const frame = g_state.frame.?;
const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: { const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: {
@ -264,6 +267,125 @@ pub fn slider(opts: SliderOptions) bool {
); );
} }
const InputF32Options = struct {
label: []const u8,
value: *f32,
ex: ?struct {
step: f32,
step_fast: f32,
format: [*c]const u8 = null,
} = null
};
pub fn inputF32(opts: InputF32Options) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
if (opts.ex) |ex| {
return ig.igInputFloatEx(
formatString("{s}", .{opts.label}),
opts.value,
ex.step,
ex.step_fast,
ex.format,
ig.ImGuiInputTextFlags_None
);
} else {
return ig.igInputFloat(
formatString("{s}", .{opts.label}),
opts.value,
);
}
}
const InputF32DragOptions = struct {
label: []const u8,
value: *f32,
ex: ?struct {
speed: f32 = 1,
min: f32 = 0,
max: f32 = 10
} = null,
};
pub fn inputF32Drag(opts: InputF32DragOptions) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
if (opts.ex) |ex| {
return ig.igDragFloatEx(
formatString("{s}", .{opts.label}),
opts.value,
ex.speed,
ex.min,
ex.max,
null,
ig.ImGuiSliderFlags_None
);
} else {
return ig.igDragFloat(
formatString("{s}", .{opts.label}),
opts.value,
);
}
}
pub fn inputDuration(label: []const u8, duration_ns: *Nanoseconds) bool {
const duration_ms: f32 = @floatFromInt(@divTrunc(duration_ns.*, std.time.ns_per_ms));
var duration_s = duration_ms / std.time.ms_per_s;
if (ImGUI.inputF32(.{
.label = label,
.value = &duration_s,
.ex = .{
.step = 0.01,
.step_fast = 0.1,
.format = "%0.3f s"
}
})) {
duration_ns.* = @trunc(duration_s * std.time.ns_per_s);
return true;
}
return false;
}
pub fn inputI32(label: []const u8, value: *i32) bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false;
}
return ig.igInputInt(
formatString("{s}", .{label}),
value,
);
}
pub fn inputU32(label: []const u8, value: *u32) bool {
var value_i32: i32 = @intCast(value.*);
if (inputI32(label, &value_i32)) {
if (value_i32 >= 0) {
value.* = @intCast(value_i32);
return true;
}
}
return false;
}
pub fn checkbox(label: []const u8, value: *bool) bool { pub fn checkbox(label: []const u8, value: *bool) bool {
if (!build_options.has_imgui) { if (!build_options.has_imgui) {
return false; return false;

View File

@ -164,68 +164,101 @@ fn PlatformType(App: type) type {
// TODO: I don't like the names of these 'event' related functions. // TODO: I don't like the names of these 'event' related functions.
// All of them "append an event", but they do different things. // All of them "append an event", but they do different things.
fn applyEventToState(self: *Self, e: Input.Event) void { fn applyEventToState(self: *Self, e: Input.Event) bool {
const input = &self.input; const input = &self.input;
switch (e) { switch (e) {
.key_pressed => |key| { .key_pressed => |key| {
assert(!input.isKeyDown(key)); if (input.isKeyDown(key)) {
return false;
}
input.keyboard.press(key); input.keyboard.press(key);
self.last_key_pressed = key; self.last_key_pressed = key;
}, },
.key_released => |key| { .key_released => |key| {
assert(input.isKeyDown(key)); if (!input.isKeyDown(key)) {
return false;
}
input.keyboard.release(key); input.keyboard.release(key);
}, },
.mouse_pressed => |button| { .mouse_pressed => |button| {
assert(input.mouse_position != null); if (input.mouse_position == null or input.isMouseDown(button)) {
assert(!input.isMouseDown(button)); return false;
}
input.mouse_buttons.press(button); input.mouse_buttons.press(button);
}, },
.mouse_released => |button| { .mouse_released => |button| {
assert(input.mouse_position != null); if (input.mouse_position == null or !input.isMouseDown(button)) {
assert(input.isMouseDown(button)); return false;
}
input.mouse_buttons.release(button); input.mouse_buttons.release(button);
}, },
.mouse_enter => |pos| { .mouse_enter => |pos| {
assert(input.mouse_position == null); if (input.mouse_position != null) {
return false;
}
input.mouse_position = pos; input.mouse_position = pos;
}, },
.mouse_move => |pos| { .mouse_move => |pos| {
assert(input.mouse_position != null); if (input.mouse_position == null) {
return false;
}
input.mouse_position = pos; input.mouse_position = pos;
}, },
.mouse_leave => { .mouse_leave => {
assert(input.mouse_position != null); if (input.mouse_position == null) {
return false;
}
input.mouse_position = null; input.mouse_position = null;
}, },
.char => |char| { .char => |char| {
// A 'key_pressed' event always occurs before a 'char' event. // A 'key_pressed' event always occurs before a 'char' event.
// This allows us to differentiate between keycodes and scan codes. // This allows us to differentiate between keycodes and scan codes.
// And how to map key codes to character codes. // And how to map key codes to character codes.
assert(self.last_key_pressed != null); if (self.last_key_pressed == null) {
return false;
}
input.key_code_mapping.put(self.last_key_pressed.?, char); input.key_code_mapping.put(self.last_key_pressed.?, char);
self.last_key_pressed = null; self.last_key_pressed = null;
}, },
.mouse_scroll => |offset| { .mouse_scroll => |offset| {
assert(input.mouse_position != null); if (input.mouse_position == null) {
return false;
}
input.mouse_scroll = input.mouse_scroll.add(offset); input.mouse_scroll = input.mouse_scroll.add(offset);
}, },
.window_resize => |window_size| { .window_resize => |window_size| {
self.input.window_size = window_size; self.input.window_size = window_size;
}, },
.focused => { .focused => {
assert(!input.focused); if (input.focused) {
return false;
}
input.focused = true; input.focused = true;
}, },
.unfocused => { .unfocused => {
assert(input.focused); if (!input.focused) {
return false;
}
assert(input.mouse_position == null); assert(input.mouse_position == null);
assert(input.keyboard.down.eql(.empty)); assert(input.keyboard.down.eql(.empty));
assert(input.mouse_buttons.down.eql(.empty)); assert(input.mouse_buttons.down.eql(.empty));
input.focused = false; input.focused = false;
} }
} }
return true;
} }
fn appendEvent(self: *Self, e: Input.Event) void { fn appendEvent(self: *Self, e: Input.Event) void {
@ -237,8 +270,9 @@ fn PlatformType(App: type) type {
return; return;
} }
self.events.appendAssumeCapacity(e); if (self.applyEventToState(e)) {
self.applyEventToState(e); self.events.appendAssumeCapacity(e);
}
} }
fn pushEvent(self: *Self, e: Input.Event) void { fn pushEvent(self: *Self, e: Input.Event) void {
@ -249,16 +283,6 @@ fn PlatformType(App: type) type {
return; return;
} }
if (e == .key_pressed and input.isKeyDown(e.key_pressed)) {
return;
}
if (e == .key_released and !input.isKeyDown(e.key_released)) {
return;
}
if (e == .mouse_move and input.mouse_position.?.eql(e.mouse_move)) {
return;
}
if (e == .unfocused) { if (e == .unfocused) {
var key_iter = input.keyboard.down.iterator(); var key_iter = input.keyboard.down.iterator();
while (key_iter.next()) |key| { while (key_iter.next()) |key| {
@ -268,9 +292,7 @@ fn PlatformType(App: type) type {
while (mouse_buttons_iter.next()) |button| { while (mouse_buttons_iter.next()) |button| {
self.appendEvent(.{ .mouse_released = button }); self.appendEvent(.{ .mouse_released = button });
} }
if (input.mouse_position != null) { self.appendEvent(.{ .mouse_leave = {} });
self.appendEvent(.{ .mouse_leave = {} });
}
} }
} else { } else {
@ -287,13 +309,6 @@ fn PlatformType(App: type) type {
} }
} }
if (e == .mouse_leave and input.mouse_position == null) {
return;
}
if (e == .mouse_enter and input.mouse_position != null) {
return;
}
self.appendEvent(e); self.appendEvent(e);
} }