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,
eternal_flame_id: EntityId,
upgrades: Upgrades,
font: Gfx.Font.Id,
idle_animations: AnimationDirections,
walk_animations: AnimationDirections,
@ -160,7 +162,6 @@ const Entity = struct {
minion_state: MinionState = .wander,
minion_chop_cooldown: Nanoseconds = 0,
minion_chop_damage: u32 = 2,
minion_throw_cooldown: Nanoseconds = 0,
minion_carried_wood: ?EntityId = null,
@ -178,9 +179,6 @@ const Entity = struct {
pos: Vec2 = .init(0, 0),
vel: 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_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 {
return Gfx.ImageData{
.width = img.width,
@ -272,6 +292,7 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.camera_pos = .init(0, 0),
.font = font,
.eternal_flame_id = undefined,
.upgrades = .{}
};
self.spawnMinion(.init(0, 0));
@ -306,9 +327,6 @@ pub fn spawnMinion(self: *App, pos: Vec2) void {
},
.pos = pos,
.friction = 0.998,
.max_velocity = 50,
.max_walk_speed = 300,
.animation = .idle,
});
@ -337,7 +355,7 @@ fn spawnTree(self: *App, station_id: EntityId) void {
return;
}
const health = 10;
const health = self.upgrades.tree_health;
if (self.spawnEntity(Entity{
.type = .tree,
.tree_health = health,
@ -353,13 +371,15 @@ fn destroyTree(self: *App, tree_id: EntityId) void {
const tree = self.entities.get(tree_id) orelse return;
assert(tree.type == .tree);
for (0..self.upgrades.tree_wood_amount) |_| {
self.spawnWood(tree.pos);
}
self.entities.removeAssumeExists(tree_id);
}
fn spawnWood(self: *App, pos: Vec2) void {
const health = 20;
const health = self.upgrades.burn_wood_health;
_ = self.spawnEntity(Entity{
.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);
}
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 {
const minion = self.entities.getAssumeExists(minion_id);
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.minion_chop_cooldown == 0) {
minion.minion_chop_cooldown = std.time.ns_per_ms * 500;
tree.tree_health -= minion.minion_chop_damage;
minion.minion_chop_cooldown = @intFromFloat(std.time.ns_per_s / self.upgrades.minion_chop_rate);
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.destroyTree(tree_id);
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| {
const wood = self.entities.getAssumeExists(wood_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.wood_burning = true;
wood.wood_carried_by = 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 {
minion.minion_throw_cooldown = @max(minion.minion_throw_cooldown - plt.dt, 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| {
const stats = self.upgrades.minion_walk;
const diff = minion.pos.x - pos.x;
if (diff < -goal_distance) {
minion.acc = .init(minion.max_walk_speed, 0);
minion.acc = .init(stats.max_speed, 0);
} 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 (!self.entities.exists(station_owned_tree)) {
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);
while (wood_iter.next()) |wood_id| {
const wood = self.entities.getAssumeExists(wood_id);
@ -720,10 +759,10 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
// Apply burn
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) {
self.heat += 1;
self.heat += self.upgrades.burn_heat_per_wood;
self.entities.removeAssumeExists(wood_id);
}
@ -881,15 +920,21 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
if (e.flags.has_kinematics) {
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));
const friction_force = std.math.pow(f32, 1 - e.friction, dt);
e.vel = e.vel.multiplyScalar(friction_force);
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) {
const animations = switch (e.animation) {
.idle => self.idle_animations,
@ -958,6 +1003,11 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
const eternal_flame = self.entities.getAssumeExists(id);
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}),
});
if (ImGUI.beginWindow(.{ .name = "Debug", .size = .init(200, 100) })) {
if (ImGUI.beginWindow(.{ .name = "Debug", .alpha = 0.8, .size = .init(500, 200) })) {
defer ImGUI.endWindow();
if (ImGUI.button("spawn")) {
if (ImGUI.button("spawn minion")) {
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 Nanoseconds = @import("./platform.zig").Nanoseconds;
const ImGUI = @This();
const State = struct {
@ -150,6 +152,7 @@ pub const WindowOptions = struct {
size: ?Vec2 = null,
collapsed: ?bool = null,
open: ?*bool = null,
alpha: f32 = 1
};
pub fn beginWindow(opts: WindowOptions) bool {
@ -175,7 +178,7 @@ pub fn beginWindow(opts: WindowOptions) bool {
ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once);
}
ig.igSetNextWindowBgAlpha(1);
ig.igSetNextWindowBgAlpha(opts.alpha);
const frame = g_state.frame.?;
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 {
if (!build_options.has_imgui) {
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.
// 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;
switch (e) {
.key_pressed => |key| {
assert(!input.isKeyDown(key));
if (input.isKeyDown(key)) {
return false;
}
input.keyboard.press(key);
self.last_key_pressed = key;
},
.key_released => |key| {
assert(input.isKeyDown(key));
if (!input.isKeyDown(key)) {
return false;
}
input.keyboard.release(key);
},
.mouse_pressed => |button| {
assert(input.mouse_position != null);
assert(!input.isMouseDown(button));
if (input.mouse_position == null or input.isMouseDown(button)) {
return false;
}
input.mouse_buttons.press(button);
},
.mouse_released => |button| {
assert(input.mouse_position != null);
assert(input.isMouseDown(button));
if (input.mouse_position == null or !input.isMouseDown(button)) {
return false;
}
input.mouse_buttons.release(button);
},
.mouse_enter => |pos| {
assert(input.mouse_position == null);
if (input.mouse_position != null) {
return false;
}
input.mouse_position = pos;
},
.mouse_move => |pos| {
assert(input.mouse_position != null);
if (input.mouse_position == null) {
return false;
}
input.mouse_position = pos;
},
.mouse_leave => {
assert(input.mouse_position != null);
if (input.mouse_position == null) {
return false;
}
input.mouse_position = null;
},
.char => |char| {
// A 'key_pressed' event always occurs before a 'char' event.
// This allows us to differentiate between keycodes and scan 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);
self.last_key_pressed = null;
},
.mouse_scroll => |offset| {
assert(input.mouse_position != null);
if (input.mouse_position == null) {
return false;
}
input.mouse_scroll = input.mouse_scroll.add(offset);
},
.window_resize => |window_size| {
self.input.window_size = window_size;
},
.focused => {
assert(!input.focused);
if (input.focused) {
return false;
}
input.focused = true;
},
.unfocused => {
assert(input.focused);
if (!input.focused) {
return false;
}
assert(input.mouse_position == null);
assert(input.keyboard.down.eql(.empty));
assert(input.mouse_buttons.down.eql(.empty));
input.focused = false;
}
}
return true;
}
fn appendEvent(self: *Self, e: Input.Event) void {
@ -237,8 +270,9 @@ fn PlatformType(App: type) type {
return;
}
if (self.applyEventToState(e)) {
self.events.appendAssumeCapacity(e);
self.applyEventToState(e);
}
}
fn pushEvent(self: *Self, e: Input.Event) void {
@ -249,16 +283,6 @@ fn PlatformType(App: type) type {
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) {
var key_iter = input.keyboard.down.iterator();
while (key_iter.next()) |key| {
@ -268,10 +292,8 @@ fn PlatformType(App: type) type {
while (mouse_buttons_iter.next()) |button| {
self.appendEvent(.{ .mouse_released = button });
}
if (input.mouse_position != null) {
self.appendEvent(.{ .mouse_leave = {} });
}
}
} else {
if (e == .unfocused) {
@ -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);
}