add tree chopping

This commit is contained in:
Rokas Puzonas 2026-07-16 01:34:53 +03:00
parent 73bf3c2d34
commit 7e3c8b4a75
4 changed files with 269 additions and 132 deletions

View File

@ -15,6 +15,8 @@ const Platform = @import("./platform.zig");
const Gfx = Platform.Gfx;
const ImGUI = Platform.ImGUI;
const Rect = Platform.Rect;
const Color = Platform.Color;
const Nanoseconds = Platform.Nanoseconds;
const SlotMapType = @import("./slot_map.zig").SlotMapType;
@ -28,6 +30,7 @@ prng: Prng,
heat: u64 = 0,
entities_buffer: [2048]EntitySlotMap.Slot,
entities: EntitySlotMap,
font: Gfx.Font.Id,
@ -149,14 +152,21 @@ const Entity = struct {
type: Type = .nil,
flags: Flags = .{},
minion_state: MinionState = .chop,
minion_target_station: ?EntityId = null,
work_progress: f32 = 0,
minion_chop_speed: f32 = 1,
target_entity: ?EntityId = null,
targeted_by_entity: ?EntityId = null,
minion_state: MinionState = .goto_tree_station,
minion_work_progress: f32 = 0,
minion_chop_cooldown: Nanoseconds = 0,
minion_chop_damage: u32 = 2,
minion_burn_speed: f32 = 1,
tree_health: u32 = 0,
tree_max_health: u32 = 0,
station_subtype: StationSubtype = .chop,
station_occupant: ?EntityId = null,
station_spawn_tree_cooldown: Nanoseconds = 0,
station_owned_tree: ?EntityId = null,
pos: Vec2 = .init(0, 0),
vel: Vec2 = .init(0, 0),
@ -168,10 +178,12 @@ const Entity = struct {
animation: AnimationName = .idle,
animation_dir: AnimationDirection = .down,
animation_index: u32 = 0,
animation_timer: Platform.Nanoseconds = 0,
animation_timer: Nanoseconds = 0,
const MinionState = enum {
chop,
goto_tree_station,
chop_tree,
pickup_wood,
burn
};
@ -179,6 +191,8 @@ const Entity = struct {
nil,
minion,
station,
tree,
wood,
};
const StationSubtype = enum {
@ -243,7 +257,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.idle_animations = try AnimationDirections.init(plt.arena, idle_sheet, ns_per_s / idle_fps, 2),
.walk_animations = try AnimationDirections.init(plt.arena, walk_sheet, ns_per_s / walk_fps, 4),
.death_animations = try AnimationDirections.init(plt.arena, death_sheet, ns_per_s / death_fps, 3),
.entities = .init(.empty),
.entities_buffer = undefined,
.entities = .init(&self.entities_buffer),
.prng = Prng.init(@truncate(@as(u96, @intCast(now.nanoseconds)))),
.camera_pos = .init(0, 0),
.font = font
@ -261,15 +276,12 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
}
pub fn spawnMinion(self: *App, pos: Vec2) !void {
const minion_id = try self.entities.insert(self.gpa);
const minion = self.entities.getAssumeExists(minion_id);
minion.* = Entity{
_ = try self.entities.insert(Entity{
.type = .minion,
.flags = .{
.has_animation = true,
.has_kinematics = true
},
.minion_state = .chop,
.pos = pos,
.friction = 0.998,
@ -277,18 +289,34 @@ pub fn spawnMinion(self: *App, pos: Vec2) !void {
.max_walk_speed = 300,
.animation = .idle,
};
});
}
pub fn spawnStation(self: *App, pos: f32, subtype: Entity.StationSubtype) !void {
const id = try self.entities.insert(self.gpa);
const station = self.entities.getAssumeExists(id);
station.* = Entity{
_ = try self.entities.insert(Entity{
.type = .station,
.station_subtype = subtype,
.pos = .init(pos, 0),
};
});
}
fn spawnTree(self: *App, station_id: EntityId) !void {
const station = self.entities.get(station_id) orelse return;
assert(station.type == .station);
if (station.station_owned_tree != null) {
return;
}
const health = 10;
const id = try self.entities.insert(Entity{
.type = .tree,
.tree_health = health,
.tree_max_health = health,
.pos = station.pos
});
station.station_owned_tree = id;
}
fn getNearestFreeStation(self: *App, from: Vec2, subtype: Entity.StationSubtype) ?EntityId {
@ -304,7 +332,7 @@ fn getNearestFreeStation(self: *App, from: Vec2, subtype: Entity.StationSubtype)
if (station.station_subtype != subtype) {
continue;
}
if (station.station_occupant != null) {
if (station.targeted_by_entity != null) {
continue;
}
@ -319,31 +347,59 @@ fn getNearestFreeStation(self: *App, from: Vec2, subtype: Entity.StationSubtype)
return result;
}
fn bindEntityToStation(self: *App, other_id: EntityId, station_id: EntityId) void {
const station = self.entities.get(station_id) orelse return;
assert(station.type == .station);
fn getNearestFreeTree(self: *App, from: Vec2) ?EntityId {
var result: ?EntityId = null;
var result_distance: f32 = 0;
const other = self.entities.get(other_id) orelse return;
assert(other.type != .station);
var station_iter = self.entities.iterator();
while (station_iter.next()) |id| {
const station = self.entities.getAssumeExists(id);
if (station.type != .tree) {
continue;
}
if (station.targeted_by_entity != null) {
continue;
}
station.station_occupant = other_id;
other.minion_target_station = station_id;
const distance = station.pos.distanceSqr(from);
if (result == null or result_distance > distance) {
result = id;
result_distance = distance;
}
}
return result;
}
fn unbindEntityFromStation(self: *App, station_id: EntityId) void {
const station = self.entities.get(station_id) orelse return;
if (station.type != .station) {
return;
fn setEntityTarget(self: *App, entity_id: EntityId, target_id: EntityId) void {
const entity = self.entities.get(entity_id) orelse return;
const target = self.entities.get(target_id) orelse return;
self.unsetEntityTarget(entity_id);
entity.target_entity = target_id;
target.targeted_by_entity = entity_id;
}
fn unsetEntityTarget(self: *App, entity_id: EntityId) void {
const entity = self.entities.get(entity_id) orelse return;
if (entity.target_entity) |target_id| {
if (self.entities.get(target_id)) |target| {
if (target.targeted_by_entity == entity_id) {
target.targeted_by_entity = null;
}
}
if (station.station_occupant) |occupant_id| {
station.station_occupant = null;
if (self.entities.get(occupant_id)) |occupant| {
occupant.minion_target_station = null;
}
entity.target_entity = null;
}
}
fn drawProgressBar(progress: f32, center: Vec2, size: Vec2, color: Color) void {
const from = center.sub(.init(size.x/2, 0));
Gfx.drawLine(from, from.add(.init(size.x * progress, 0)), color, size.y);
}
pub fn frame(self: *App, plt: Platform.Frame) !void {
const input = plt.input;
_ = input; // autofix
@ -368,21 +424,56 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
Gfx.drawLine(.init(-800, 0), .init(800, 0), .white, 5);
var station_iter = self.entities.iterator();
while (station_iter.next()) |minion_id| {
const station = self.entities.getAssumeExists(minion_id);
while (station_iter.next()) |station_id| {
const station = self.entities.getAssumeExists(station_id);
if (station.type != .station) {
continue;
}
if (station.station_occupant) |station_occupant| {
if (!self.entities.exists(station_occupant)) {
station.station_occupant = null;
if (station.station_owned_tree) |station_owned_tree| {
if (!self.entities.exists(station_owned_tree)) {
station.station_owned_tree = null;
station.station_spawn_tree_cooldown = std.time.ns_per_s;
}
}
if (station.station_subtype == .chop) {
station.station_spawn_tree_cooldown = @max(station.station_spawn_tree_cooldown - plt.dt, 0);
if (station.station_spawn_tree_cooldown == 0 and station.station_owned_tree == null) {
try self.spawnTree(station_id);
}
}
}
const character_size = Vec2.init(96, 96);
{
var entity_iter = self.entities.iterator();
while (entity_iter.next()) |entity_id| {
const e = self.entities.getAssumeExists(entity_id);
if (e.target_entity) |other_id| {
if (self.entities.get(other_id)) |other| {
if (other.targeted_by_entity != entity_id) {
e.target_entity = null;
}
} else {
e.target_entity = null;
}
}
if (e.targeted_by_entity) |other_id| {
if (self.entities.get(other_id)) |other| {
if (other.target_entity != entity_id) {
e.targeted_by_entity = null;
}
} else {
e.targeted_by_entity = null;
}
}
}
}
var minion_iter = self.entities.iterator();
while (minion_iter.next()) |minion_id| {
const minion = self.entities.getAssumeExists(minion_id);
@ -390,18 +481,69 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
continue;
}
if (minion.minion_target_station) |minion_target_station| {
if (!self.entities.exists(minion_target_station)) {
minion.minion_target_station = null;
}
}
const goal_distance_threshold = 10;
var walk_goal: ?Vec2 = null;
var station_goal: ?Entity.StationSubtype = null;
switch (minion.minion_state) {
.goto_tree_station => {
// TODO: Steal tree is it is nearer to you compared to the current minion that has it reserved.
if (self.getNearestFreeTree(minion.pos)) |tree_id| {
self.setEntityTarget(minion_id, tree_id);
minion.minion_state = .chop_tree;
} else {
station_goal = .chop;
if (minion.target_entity) |station_id| {
const station = self.entities.getAssumeExists(station_id);
if (station.type != .station) {
self.unsetEntityTarget(minion_id);
}
}
}
},
.chop_tree => {
if (minion.target_entity) |tree_id| {
if (self.entities.getAssumeExists(tree_id).type != .tree) {
self.unsetEntityTarget(minion_id);
minion.minion_state = .goto_tree_station;
}
}
var is_at_tree = false;
if (minion.target_entity) |tree_id| {
const tree = self.entities.getAssumeExists(tree_id);
is_at_tree = minion.pos.distance(tree.pos) < goal_distance_threshold;
if (is_at_tree) {
minion.minion_chop_cooldown = @max(minion.minion_chop_cooldown - plt.dt, 0);
if (minion.minion_chop_cooldown == 0) {
minion.minion_chop_cooldown = std.time.ns_per_ms * 500;
tree.tree_health -= minion.minion_chop_damage;
}
if (tree.tree_health <= 0) {
minion.minion_state = .burn;
self.unsetEntityTarget(minion_id);
self.entities.removeAssumeExists(tree_id);
}
} else {
walk_goal = tree.pos;
}
}
if (!is_at_tree) {
minion.minion_work_progress = 0;
}
},
.pickup_wood => {
},
.burn => {
station_goal = .burn;
var is_at_station = false;
if (minion.minion_target_station) |station_id| {
if (minion.target_entity) |station_id| {
const station = self.entities.getAssumeExists(station_id);
if (station.pos.distance(minion.pos) < goal_distance_threshold) {
is_at_station = true;
@ -409,55 +551,44 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
}
if (!is_at_station) {
minion.work_progress = 0;
minion.minion_work_progress = 0;
}
switch (minion.minion_state) {
.chop => {
station_goal = .chop;
if (is_at_station) {
minion.work_progress += minion.minion_chop_speed * dt;
minion.minion_work_progress += minion.minion_burn_speed * dt;
}
minion.work_progress = std.math.clamp(minion.work_progress, 0, 1);
if (minion.work_progress == 1) {
minion.minion_state = .burn;
minion.work_progress = 0;
}
},
.burn => {
station_goal = .burn;
if (is_at_station) {
minion.work_progress += minion.minion_burn_speed * dt;
}
minion.work_progress = std.math.clamp(minion.work_progress, 0, 1);
if (minion.work_progress == 1) {
minion.minion_state = .chop;
minion.work_progress = 0;
minion.minion_work_progress = std.math.clamp(minion.minion_work_progress, 0, 1);
if (minion.minion_work_progress == 1) {
minion.minion_state = .goto_tree_station;
minion.minion_work_progress = 0;
self.heat += 1;
}
},
}
if (station_goal) |station_type| {
if (minion.minion_target_station) |station_id| {
if (minion.target_entity) |entity_id| {
if (self.entities.get(entity_id)) |station| {
if (station.type != .station) {
}
}
}
if (minion.target_entity) |station_id| {
const station = self.entities.getAssumeExists(station_id);
if (station.station_subtype != station_type) {
self.unbindEntityFromStation(station_id);
minion.minion_target_station = null;
self.unsetEntityTarget(minion_id);
}
}
if (minion.minion_target_station == null) {
if (minion.target_entity == null) {
if (self.getNearestFreeStation(minion.pos, station_type)) |station_id| {
self.bindEntityToStation(minion_id, station_id);
self.setEntityTarget(minion_id, station_id);
}
}
if (minion.minion_target_station) |station_id| {
if (minion.target_entity) |station_id| {
const station = self.entities.getAssumeExists(station_id);
walk_goal = station.pos;
}
@ -484,6 +615,21 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
}
}
{
var tree_iter = self.entities.iterator();
while (tree_iter.next()) |id| {
const e = self.entities.getAssumeExists(id);
if (e.type == .tree) {
const tree_size = Vec2.init(40, 100);
Gfx.drawRectangle(e.pos.sub(.init(tree_size.x/2, tree_size.y)), tree_size, .rgb(40, 200, 40));
const progress = @as(f32, @floatFromInt(e.tree_health)) / @as(f32, @floatFromInt(e.tree_max_health));
drawProgressBar(progress, e.pos.add(.init(0, 20)), .init(40, 10), .rgb(0, 100, 20));
}
}
}
var entity_iter = self.entities.iterator();
while (entity_iter.next()) |id| {
const e = self.entities.getAssumeExists(id);
@ -542,12 +688,9 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
);
Gfx.drawSprite(sprite, pos, size, .white);
const is_working = e.minion_state == .chop or e.minion_state == .burn;
if (e.type == .minion and is_working and e.work_progress > 0) {
const progressbar_size = 100;
const from = e.pos.add(.init(-progressbar_size/2, 20));
Gfx.drawLine(from, from.add(.init(progressbar_size * e.work_progress, 0)), .rgb(0, 200, 20), 10);
const is_working = e.minion_state == .chop_tree or e.minion_state == .burn;
if (e.type == .minion and is_working and e.minion_work_progress > 0) {
drawProgressBar(e.minion_work_progress, e.pos.add(.init(0, 20)), .init(100, 10), .rgb(0, 200, 20));
}
}
@ -557,6 +700,12 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
} else {
Gfx.drawRectangle(e.pos.sub(.init(5, 5)), .init(10, 10), .rgb(200, 20, 20));
}
} else if (e.type == .minion) {
Gfx.drawText(self.font, .{
.pos = e.pos.sub(.init(20, 50)),
.height = 16,
.text = try std.fmt.allocPrint(plt.arena, "{}", .{e.minion_state})
});
}
}
@ -576,6 +725,6 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
}
pub fn deinit(self: *App, plt: Platform.Deinit) void {
self.entities.deinit(self.gpa);
_ = self; // autofix
_ = plt; // autofix
}

View File

@ -168,19 +168,19 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
.default_sampler = .nearest,
.textures_buffer = undefined,
.textures = .init(.initBuffer(&self.textures_buffer)),
.textures = .init(&self.textures_buffer),
.nil_texture = undefined,
.sprites_buffer = undefined,
.sprites = .init(.initBuffer(&self.sprites_buffer)),
.sprites = .init(&self.sprites_buffer),
.nil_sprite = undefined,
.spritesheets_buffer = undefined,
.spritesheets = .init(.initBuffer(&self.spritesheets_buffer)),
.spritesheets = .init(&self.spritesheets_buffer),
.nil_spritesheet = undefined,
.fonts_buffer = undefined,
.fonts = .init(.initBuffer(&self.fonts_buffer)),
.fonts = .init(&self.fonts_buffer),
.nil_font = undefined,
};
@ -511,7 +511,7 @@ const TextureOptions = struct {
pub fn initTexture(opts: TextureOptions) Texture.Id {
const self = &g_state;
const id = self.textures.insertBounded() catch {
const id = self.textures.insertUndefined() catch {
log.warn("Failed to create texture, limit reached! limit: {}", .{self.textures.slots.capacity});
return self.nil_texture;
};
@ -765,7 +765,7 @@ pub fn getNilSprite() Sprite.Id {
pub fn initSprite(opts: SpriteOptions) Sprite.Id {
const self = &g_state;
const id = self.sprites.insertBounded() catch {
const id = self.sprites.insertUndefined() catch {
log.warn("Failed to create sprite, limit reached! limit: {}", .{self.sprites.slots.capacity});
return self.nil_sprite;
};
@ -1125,7 +1125,7 @@ pub const SpriteSheetOptions = struct {
pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id {
const self = &g_state;
const id = self.spritesheets.insertBounded() catch {
const id = self.spritesheets.insertUndefined() catch {
log.warn("Failed to create spritesheet, limit reached! limit: {}", .{self.spritesheets.slots.capacity});
return self.nil_spritesheet;
};
@ -1159,7 +1159,7 @@ pub fn deinitSpritesheet(id: Spritesheet.Id) void {
pub fn initFont() Font.Id {
const self = &g_state;
const id = self.fonts.insertBounded() catch {
const id = self.fonts.insertUndefined() catch {
log.warn("Failed to create font, limit reached! limit: {}", .{self.fonts.slots.capacity});
return self.nil_font;
};

View File

@ -287,6 +287,13 @@ 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);
}

View File

@ -78,10 +78,7 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
};
pub fn clearRetainingCapacity(self: *Self) void {
var slots = self.slots;
slots.clearRetainingCapacity();
self.* = .init(slots);
self.* = .init(self.slots.items);
}
fn insertHole(self: *Self, index: Index) void {
@ -121,19 +118,6 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
return null;
}
pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) Allocator.Error!void {
if (additional_count > self.hole_count) {
const new_capacity, const overflow = @addWithOverflow(
self.slots.capacity,
additional_count - self.hole_count
);
if (overflow != 0) return error.OutOfMemory;
if (new_capacity >= std.math.maxInt(Index)) return error.OutOfMemory;
try self.slots.ensureTotalCapacity(gpa, new_capacity);
}
}
pub fn unusedCapacity(self: *Self) usize {
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
return capacity - self.slots.items.len + self.hole_count;
@ -151,18 +135,19 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
};
}
pub fn insert(self: *Self, gpa: Allocator) Allocator.Error!Id {
try self.ensureUnusedCapacity(gpa, 1);
return self.insertAssumeCapacity();
}
pub fn insertBounded(self: *Self) Allocator.Error!Id {
pub fn insertUndefined(self: *Self) Allocator.Error!Id {
if (self.unusedCapacity() == 0) {
return error.OutOfMemory;
}
return self.insertAssumeCapacity();
}
pub fn insert(self: *Self, value: Value) Allocator.Error!Id {
const id = try self.insertUndefined();
self.getAssumeExists(id).* = value;
return id;
}
pub fn exists(self: *Self, id: Id) bool {
if (id.index >= self.slots.items.len) {
return false;
@ -211,15 +196,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
};
}
pub fn init(slots: std.ArrayList(Slot)) Self {
pub fn init(slots: []Slot) Self {
var self: Self = .empty;
self.slots = slots;
self.slots = .initBuffer(slots);
return self;
}
pub fn deinit(self: *Self, gpa: Allocator) void {
self.slots.deinit(gpa);
}
};
}
@ -234,13 +215,13 @@ test "insert & remove" {
var map: TestMap = .empty;
defer map.deinit(gpa);
const id1 = try map.insert(gpa);
const id1 = try map.insertUndefined(gpa);
try expect(map.exists(id1));
try expect(map.remove(id1));
try expect(!map.exists(id1));
try expect(!map.remove(id1));
const id2 = try map.insert(gpa);
const id2 = try map.insertUndefined(gpa);
try expect(map.exists(id2));
try expect(!map.exists(id1));
}
@ -253,14 +234,14 @@ test "generation wrap around" {
defer map.deinit(gpa);
// Grow array list so that at least 1 slot exists
const id1 = try map.insert(gpa);
const id1 = try map.insertUndefined(gpa);
map.removeAssumeExists(id1);
// Artificially increase generation count
map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation"));
// Check if generation wraps around
const id2 = try map.insert(gpa);
const id2 = try map.insertUndefined(gpa);
map.removeAssumeExists(id2);
try expectEqual(id1.index, id2.index);
try expectEqual(0, map.slots.items[id1.index].generation);
@ -274,9 +255,9 @@ test "iterator" {
defer map.deinit(gpa);
// Create array which has a hole
const id1 = try map.insert(gpa);
const id2 = try map.insert(gpa);
const id3 = try map.insert(gpa);
const id1 = try map.insertUndefined(gpa);
const id2 = try map.insertUndefined(gpa);
const id3 = try map.insertUndefined(gpa);
map.removeAssumeExists(id2);
@ -294,11 +275,11 @@ test "clear retaining capacity" {
var map: TestMap = .empty;
defer map.deinit(gpa);
const id1 = try map.insert(gpa);
const id1 = try map.insertUndefined(gpa);
try expect(map.exists(id1));
map.clearRetainingCapacity();
const id2 = try map.insert(gpa);
const id2 = try map.insertUndefined(gpa);
try expect(map.exists(id2));
try expectEqual(id1, id2);