add fire animation

This commit is contained in:
Rokas Puzonas 2026-07-19 06:50:20 +03:00
parent 19e37f507d
commit 3c61d2164d
7 changed files with 306 additions and 132 deletions

BIN
assets/fire/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/fire/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
assets/fire/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
assets/fire/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/fire/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -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 });

View File

@ -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);
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;
}
if (e.flags.has_kinematics) {
e.acc = .init(0, 0);
}
}
}
{
var minion_iter = self.entities.iterator();
{ // Minion logic & animations
var minion_iter = self.initEntityIteratorByType(.minion);
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;
minion.minion_animation = .walk;
minion.minion_animation_dir = .right;
} else if (minion.acc.x < 0) {
minion.animation = .walk;
minion.animation_dir = .left;
minion.minion_animation = .walk;
minion.minion_animation_dir = .left;
} else {
minion.animation = .idle;
minion.animation_dir = .down;
minion.minion_animation = .idle;
minion.minion_animation_dir = .down;
}
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);
}
}
}
}
{
{ // 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)),