diff --git a/assets/fire/1.png b/assets/fire/1.png new file mode 100644 index 0000000..f75fe5d Binary files /dev/null and b/assets/fire/1.png differ diff --git a/assets/fire/2.png b/assets/fire/2.png new file mode 100644 index 0000000..278d870 Binary files /dev/null and b/assets/fire/2.png differ diff --git a/assets/fire/3.png b/assets/fire/3.png new file mode 100644 index 0000000..a56e994 Binary files /dev/null and b/assets/fire/3.png differ diff --git a/assets/fire/4.png b/assets/fire/4.png new file mode 100644 index 0000000..4eed33b Binary files /dev/null and b/assets/fire/4.png differ diff --git a/assets/fire/5.png b/assets/fire/5.png new file mode 100644 index 0000000..1b8abb7 Binary files /dev/null and b/assets/fire/5.png differ diff --git a/build.zig b/build.zig index caaaceb..0c926ba 100644 --- a/build.zig +++ b/build.zig @@ -12,7 +12,10 @@ pub fn build(b: *std.Build) !void { "Prototype_Character/Default/walk.png", "Prototype_Character/Default/death.png", "font/pixeloid/PixeloidSans.ttf", - "tranquil_tunnels_transparent.png" + "tranquil_tunnels_transparent.png", + "fire/1.png", + "fire/2.png", + "fire/5.png", }; const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir }); diff --git a/src/app.zig b/src/app.zig index 52e5fd7..a96a25d 100644 --- a/src/app.zig +++ b/src/app.zig @@ -43,11 +43,16 @@ font: Gfx.Font.Id, idle_animations: AnimationDirections, walk_animations: AnimationDirections, death_animations: AnimationDirections, +fire1_animation: Animation, +fire2_animation: Animation, +fire5_animation: Animation, gradient_sprite: Gfx.Sprite.Id, terrain_sheet: Tilesheet, terrain_tiles: []Tile, +show_minion_state: bool = false, + const terrain_origin_x: i32 = -80; const terrain_origin_y: i32 = -60; const terrain_width = 160; @@ -64,7 +69,9 @@ const Tilesheet = struct { width: u32, height: u32, - pub fn init(gpa: Allocator, image: Gfx.ImageData, width: u32, height: u32, tile_size: Vec2) !Tilesheet { + pub fn init(gpa: Allocator, image: Gfx.ImageData, tile_size: Vec2) !Tilesheet { + const width = @divFloor(image.width, @as(u32, @intFromFloat(tile_size.x))); + const height = @divFloor(image.height, @as(u32, @intFromFloat(tile_size.y))); const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height); errdefer gpa.free(sprites); @memset(sprites, Gfx.getNilSprite()); @@ -179,6 +186,8 @@ const Entity = struct { minion_carried_wood_len: u32 = 0, minion_wander_target: ?Vec2 = null, minion_wander_cooldown: Nanoseconds = 0, + minion_animation: AnimationName = .idle, + minion_animation_dir: AnimationDirection = .down, tree_health: u32 = 0, tree_visual_health: f32 = 0, @@ -195,12 +204,15 @@ const Entity = struct { wood_next_carried: ?EntityId = null, wood_visual_pos: Vec2 = .init(0, 0), + eternal_flame_size: FlameSize = .small, + pos: Vec2 = .init(0, 0), vel: Vec2 = .init(0, 0), acc: Vec2 = .init(0, 0), - animation: AnimationName = .idle, - animation_dir: AnimationDirection = .down, + animation: ?Animation = null, + animation_rect: Rect = .init(0, 0, 0, 0), + animation_flip: bool = false, animation_index: u32 = 0, animation_timer: Nanoseconds = 0, @@ -226,9 +238,14 @@ const Entity = struct { eternal_flame, }; + const FlameSize = enum { + small, + medium, + large, + }; + const Flags = packed struct { has_kinematics: bool = false, - has_animation: bool = false }; }; @@ -436,35 +453,45 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { const idle_sheet = try Tilesheet.init( plt.arena, initImageDataFromSTB(try STBImage.load(plt.assets.readFile("Prototype_Character/Default/idle.png"))), - 2, - 3, .init(32, 32) ); const walk_sheet = try Tilesheet.init( plt.arena, initImageDataFromSTB(try STBImage.load(plt.assets.readFile("Prototype_Character/Default/walk.png"))), - 4, - 3, .init(32, 32) ); const death_sheet = try Tilesheet.init( plt.arena, initImageDataFromSTB(try STBImage.load(plt.assets.readFile("Prototype_Character/Default/death.png"))), - 3, - 3, .init(32, 32) ); const terrain_sheet = try Tilesheet.init( plt.arena, initImageDataFromSTB(try STBImage.load(plt.assets.readFile("tranquil_tunnels_transparent.png"))), - 128, - 128, .init(8, 8) ); + const fire1_sheet = try Tilesheet.init( + plt.arena, + initImageDataFromSTB(try STBImage.load(plt.assets.readFile("fire/1.png"))), + .init(16, 32) + ); + + const fire2_sheet = try Tilesheet.init( + plt.arena, + initImageDataFromSTB(try STBImage.load(plt.assets.readFile("fire/2.png"))), + .init(32, 48) + ); + + const fire5_sheet = try Tilesheet.init( + plt.arena, + initImageDataFromSTB(try STBImage.load(plt.assets.readFile("fire/5.png"))), + .init(32, 48) + ); + const terrain_tiles = try plt.arena.alloc(Tile, terrain_width*terrain_height); const ns_per_s = std.time.ns_per_s; @@ -496,6 +523,36 @@ 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), + .fire1_animation = try Animation.init(plt.arena, fire1_sheet, std.time.ns_per_ms * 100, &.{ + .init(0, 0), + .init(1, 0), + .init(2, 0), + .init(3, 0), + .init(4, 0), + .init(5, 0), + .init(6, 0), + .init(7, 0), + }), + .fire2_animation = try Animation.init(plt.arena, fire2_sheet, std.time.ns_per_ms * 100, &.{ + .init(0, 0), + .init(1, 0), + .init(2, 0), + .init(3, 0), + .init(4, 0), + .init(5, 0), + .init(6, 0), + .init(7, 0), + }), + .fire5_animation = try Animation.init(plt.arena, fire5_sheet, std.time.ns_per_ms * 100, &.{ + .init(0, 0), + .init(1, 0), + .init(2, 0), + .init(3, 0), + .init(4, 0), + .init(5, 0), + .init(6, 0), + .init(7, 0), + }), .entities_buffer = undefined, .entities = .init(&self.entities_buffer), .prng = Prng.init(@truncate(@as(u96, @intCast(now.nanoseconds)))), @@ -518,7 +575,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { self.eternal_flame_id = self.spawnEntity(Entity{ .type = .eternal_flame, - .pos = .init(200, 0) + .pos = .init(250, 0), + .eternal_flame_size = .large, }).?; return null; @@ -695,16 +753,21 @@ fn spawnEntity(self: *App, e: Entity) ?EntityId { } pub fn spawnMinion(self: *App, pos: Vec2) void { + const character_size = Vec2.init(96, 96); + _ = self.spawnEntity(Entity{ .type = .minion, .flags = .{ - .has_animation = true, .has_kinematics = true }, .pos = pos, - .animation = .idle, + .minion_animation = .idle, + .animation_rect = .{ + .pos = .init(-character_size.x/2, -character_size.y*0.7), + .size = character_size, + } }); } @@ -741,7 +804,7 @@ fn spawnTree(self: *App, station_id: EntityId) void { .tree_max_health = health, .seed = rng.int(u64), - .pos = station.pos + .pos = station.pos.add(.init((rng.float(f32)-0.5) * terrain_tile_size.x, 0)) })) |id| { station.station_owned_tree = id; } @@ -1085,7 +1148,11 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { const wood = self.entities.getAssumeExists(wood_id); const eternal_flame = self.entities.getAssumeExists(self.eternal_flame_id); + var rng = self.prng.random(); + wood.pos = eternal_flame.pos; + wood.pos.x += (rng.float(f32)-0.5) * 2 * 30; + wood.pos.y -= rng.float(f32) * 30; wood.wood_burning = true; threw_anything = true; } @@ -1391,6 +1458,8 @@ fn showDebug(self: *App, plt: Platform.Frame) !void { self.heat = @intCast(heat); } + _ = ImGUI.checkbox("Minion state", &self.show_minion_state); + ImGUI.separator(); _ = ImGUI.text("Minion upgrades", .{}); _ = ImGUI.inputU32("count", &self.upgrades.minion_count); @@ -1469,7 +1538,7 @@ fn showDebug(self: *App, plt: Platform.Frame) !void { var minion_iter = self.initEntityIteratorByType(.minion); while (minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); - ImGUI.text("{f}, wood_head: {?f}", .{minion_id, minion.minion_carried_wood_head}); + ImGUI.text("{f}, len: {}, wood_head: {?f}", .{minion_id, minion.minion_carried_wood_len, minion.minion_carried_wood_head}); } ImGUI.separator(); @@ -1479,6 +1548,16 @@ fn showDebug(self: *App, plt: Platform.Frame) !void { ImGUI.text("{f}, carried_by: {?f}, next: {?f}", .{wood_id, wood.wood_carried_by, wood.wood_next_carried}); } } + + if (ImGUI.beginTabItem("Minions")) { + defer ImGUI.endTabItem(); + + var minion_iter = self.initEntityIteratorByType(.minion); + while (minion_iter.next()) |minion_id| { + const minion = self.entities.getAssumeExists(minion_id); + ImGUI.text("{f}, animation: {}, acc: {}, vel: {}", .{minion_id, minion.minion_animation, minion.acc.length(), minion.vel.length()}); + } + } } } @@ -1490,7 +1569,6 @@ fn drawTree(self: *App, seed: u64, pos: Vec2, health_percent: f32) void { defer Gfx.transformPop(); Gfx.transformTranslate(pos); - Gfx.transformTranslate(.init((rng.float(f32)-0.5) * terrain_tile_size.x, 0)); { Gfx.transformPush(); @@ -1553,6 +1631,28 @@ fn drawTree(self: *App, seed: u64, pos: Vec2, health_percent: f32) void { } } +fn drawEntityAnimation(self: *App, e: *Entity, dt: Nanoseconds) void { + _ = self; // autofix + if (e.animation) |animation| { + e.animation_timer += dt; + while (e.animation_timer >= animation.frame_duration) { + e.animation_timer -= animation.frame_duration; + e.animation_index += 1; + } + + e.animation_index = e.animation_index % @as(u32, @intCast(animation.frames.len)); + const sprite = animation.frames[e.animation_index]; + + var rect = e.animation_rect; + if (e.animation_flip) { + rect.pos.x += rect.size.x; + rect.size.x *= -1; + } + + Gfx.drawSprite(sprite, e.pos.add(rect.pos), rect.size, .white); + } +} + pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; _ = input; // autofix @@ -1655,6 +1755,17 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } + { + var minion_iter = self.initEntityIteratorByType(.minion); + while (minion_iter.next()) |minion_id| { + const minion = self.entities.getAssumeExists(minion_id); + + if (minion.minion_state == .wander and minion.target_entity != null) { + self.unsetEntityTarget(minion_id); + } + } + } + { // Ensure that `target_entity` and `targeted_by_entity` don't point to dead entities var entity_iter = self.entities.iterator(); while (entity_iter.next()) |entity_id| { @@ -1722,6 +1833,27 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } + { // Each burn station will assign itself to the nearest minion that has wood + var burn_station_iter = self.initEntityIteratorByType(.burn_station); + while (burn_station_iter.next()) |burn_station_id| { + const burn_station = self.entities.getAssumeExists(burn_station_id); + if (burn_station.targeted_by_entity != null) { + continue; + } + + var minion_iter = self.initNearestEntityIter(.minion, burn_station.pos, plt.arena); + while (try minion_iter.next()) |minion_id| { + const minion = self.entities.getAssumeExists(minion_id); + const state = minion.minion_state; + if (state == .wander and minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { + self.setEntityTarget(minion_id, burn_station_id); + minion.minion_state = .goto_burn_station; + break; + } + } + } + } + { // Each tree entity will assign itself to the nearest minion that isn't busy var tree_iter = self.initEntityIteratorByType(.tree); while (tree_iter.next()) |tree_id| { @@ -1786,51 +1918,79 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } - { // Each burn station will assign itself to the nearest minion that has wood - var burn_station_iter = self.initEntityIteratorByType(.burn_station); - while (burn_station_iter.next()) |burn_station_id| { - const burn_station = self.entities.getAssumeExists(burn_station_id); - if (burn_station.targeted_by_entity != null) { - continue; + { // Clear acceleration + var entity_iter = self.entities.iterator(); + while (entity_iter.next()) |id| { + const e = self.entities.getAssumeExists(id); + + if (e.flags.has_kinematics) { + e.acc = .init(0, 0); + } + } + } + + { // Minion logic & animations + var minion_iter = self.initEntityIteratorByType(.minion); + while (minion_iter.next()) |minion_id| { + const minion = self.entities.getAssumeExists(minion_id); + + try self.minionLogic(plt, minion_id); + + if (minion.acc.x > 0) { + minion.minion_animation = .walk; + minion.minion_animation_dir = .right; + } else if (minion.acc.x < 0) { + minion.minion_animation = .walk; + minion.minion_animation_dir = .left; + } else { + minion.minion_animation = .idle; + minion.minion_animation_dir = .down; } - var minion_iter = self.initNearestEntityIter(.minion, burn_station.pos, plt.arena); - while (try minion_iter.next()) |minion_id| { - const minion = self.entities.getAssumeExists(minion_id); - const state = minion.minion_state; - if (state == .wander and minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { - self.setEntityTarget(minion_id, burn_station_id); - minion.minion_state = .goto_burn_station; - break; + const animations = switch (minion.minion_animation) { + .idle => self.idle_animations, + .walk => self.walk_animations, + .death => self.death_animations + }; + + minion.animation_flip = false; + minion.animation = switch (minion.minion_animation_dir) { + .right => animations.right, + .up => animations.up, + .down => animations.down, + .left => blk: { + minion.animation_flip = true; + break :blk animations.right; + } + }; + } + } + + { // Apply kinamatics + var entity_iter = self.entities.iterator(); + while (entity_iter.next()) |id| { + const e = self.entities.getAssumeExists(id); + + if (e.flags.has_kinematics) { + e.vel = e.vel.add(e.acc.multiplyScalar(dt)); + e.pos = e.pos.add(e.vel.multiplyScalar(dt)); + + e.pos.y = @min(e.pos.y, 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); } } } } - { - var minion_iter = self.entities.iterator(); - while (minion_iter.next()) |minion_id| { - const minion = self.entities.getAssumeExists(minion_id); - if (minion.type != .minion) { - continue; - } - - try self.minionLogic(plt, minion_id); - - if (minion.acc.x > 0) { - minion.animation = .walk; - minion.animation_dir = .right; - } else if (minion.acc.x < 0) { - minion.animation = .walk; - minion.animation_dir = .left; - } else { - minion.animation = .idle; - minion.animation_dir = .down; - } - } - } - - { + { // Wood position follow var wood_iter = self.initEntityIteratorByType(.wood); while (wood_iter.next()) |wood_id| { const wood = self.entities.getAssumeExists(wood_id); @@ -1895,86 +2055,97 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } - const character_size = Vec2.init(96, 96); - { - var entity_iter = self.entities.iterator(); - while (entity_iter.next()) |id| { - const e = self.entities.getAssumeExists(id); - - if (e.flags.has_kinematics) { - e.vel = e.vel.add(e.acc.multiplyScalar(dt)); - e.pos = e.pos.add(e.vel.multiplyScalar(dt)); - - e.pos.y = @min(e.pos.y, 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) { - const animations = switch (e.animation) { - .idle => self.idle_animations, - .walk => self.walk_animations, - .death => self.death_animations - }; - var flip_animation = false; - const animation = switch (e.animation_dir) { - .right => animations.right, - .up => animations.up, - .down => animations.down, - .left => blk: { - flip_animation = true; - break :blk animations.right; - } - }; - - e.animation_timer += plt.dt; - while (e.animation_timer >= animation.frame_duration) { - e.animation_timer -= animation.frame_duration; - e.animation_index += 1; - } - - e.animation_index = e.animation_index % @as(u32, @intCast(animation.frames.len)); - const sprite = animation.frames[e.animation_index]; - - var size = character_size; - var pos = e.pos; - pos.y -= character_size.y*0.7; - if (flip_animation) { - size.x *= -1; - pos.x += character_size.x/2; - } else { - pos.x -= character_size.x/2; - } - - Gfx.drawSprite(sprite, pos, size, .white); - - if (e.type == .minion and e.minion_state == .burn_wood and e.minion_throw_cooldown > 0) { - Gfx.drawRectangle(e.pos.sub(.init(5, 65)), .init(10, 10), .rgb(200, 20, 20)); - } - } - - if (e.type == .burn_station) { - Gfx.drawRectangle(e.pos.sub(.init(5, 5)), .init(10, 10), .rgb(200, 20, 20)); - } - } - } - { // Draw eternal flame var eternal_flame_iter = self.initEntityIteratorByType(.eternal_flame); while (eternal_flame_iter.next()) |id| { const eternal_flame = self.entities.getAssumeExists(id); - Gfx.drawRectangle(eternal_flame.pos.sub(.init(10, 50)), .init(30, 50), .rgb(150, 30, 20)); + { // Draw holder + Gfx.transformPush(); + defer Gfx.transformPop(); + Gfx.transformTranslate(eternal_flame.pos); + Gfx.transformScale(terrain_tile_size); + Gfx.transformTranslate(.init(-1.5, -1)); + + Gfx.drawSprite( + self.terrain_sheet.get(118, 90), + .init(0, 0), + .init(1, 1), + .white + ); + Gfx.drawSprite( + self.terrain_sheet.get(119, 90), + .init(1, 0), + .init(1, 1), + .white + ); + Gfx.drawSprite( + self.terrain_sheet.get(120, 90), + .init(2, 0), + .init(1, 1), + .white + ); + } + + switch (eternal_flame.eternal_flame_size) { + .small => { + const flame_size = Vec2.init(16, 32).multiplyScalar(4); + eternal_flame.animation = self.fire1_animation; + eternal_flame.animation_rect = .init(-flame_size.x/2, -flame_size.y, flame_size.x, flame_size.y); + }, + .medium => { + const flame_size = Vec2.init(32, 48).multiplyScalar(4); + eternal_flame.animation = self.fire2_animation; + eternal_flame.animation_rect = .init(-flame_size.x/2, -flame_size.y, flame_size.x, flame_size.y); + }, + .large => { + const flame_size = Vec2.init(32, 48).multiplyScalar(4); + eternal_flame.animation = self.fire5_animation; + eternal_flame.animation_rect = .init(-flame_size.x/2, -flame_size.y, flame_size.x, flame_size.y); + } + } + + self.drawEntityAnimation(eternal_flame, plt.dt); + } + } + + { + var entity_iter = self.entities.iterator(); + while (entity_iter.next()) |id| { + const e = self.entities.getAssumeExists(id); + + if (e.type != .eternal_flame) { + self.drawEntityAnimation(e, plt.dt); + } + + if (e.type == .minion and e.minion_state == .burn_wood and e.minion_throw_cooldown > 0) { + Gfx.drawRectangle(e.pos.sub(.init(5, 65)), .init(10, 10), .rgb(200, 20, 20)); + } + + if (e.type == .burn_station) { + Gfx.drawSprite( + self.terrain_sheet.get(123, 81), + e.pos.sub(.init(terrain_tile_size.x/2, terrain_tile_size.y)), + terrain_tile_size, + .white + ); + } + + if (e.type == .minion and self.show_minion_state) { + Gfx.drawText(self.font, .{ + .height = 16, + .text = try std.fmt.allocPrint(plt.arena, "{}", .{e.minion_state}), + .pos = e.pos.add(.init(0, 20)), + }); + } + } + } + + { // Draw eternal flame stats + var eternal_flame_iter = self.initEntityIteratorByType(.eternal_flame); + while (eternal_flame_iter.next()) |id| { + const eternal_flame = self.entities.getAssumeExists(id); + Gfx.drawText(self.font, .{ .text = try std.fmt.allocPrint(plt.arena, "Stack: {}", .{self.countEternalFlameStackSize()}), .pos = eternal_flame.pos.sub(.init(10, 80)),