From 46ca2e5e1e09dbd686365c2801d91420088798f1 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Thu, 16 Jul 2026 23:14:25 +0300 Subject: [PATCH] implement wood burning --- src/app.zig | 597 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 399 insertions(+), 198 deletions(-) diff --git a/src/app.zig b/src/app.zig index 5a456af..ac8ca4c 100644 --- a/src/app.zig +++ b/src/app.zig @@ -24,14 +24,17 @@ const App = @This(); const Prng = std.Random.DefaultPrng; +const max_entities = 2048; + gpa: Allocator, camera_pos: Vec2, prng: Prng, heat: u64 = 0, -entities_buffer: [2048]EntitySlotMap.Slot, +entities_buffer: [max_entities]EntitySlotMap.Slot, entities: EntitySlotMap, +eternal_flame_id: EntityId, font: Gfx.Font.Id, idle_animations: AnimationDirections, @@ -155,21 +158,22 @@ const Entity = struct { target_entity: ?EntityId = null, targeted_by_entity: ?EntityId = null, - minion_state: MinionState = .goto_tree_station, - minion_work_progress: f32 = 0, + minion_state: MinionState = .wander, minion_chop_cooldown: Nanoseconds = 0, minion_chop_damage: u32 = 2, - minion_burn_speed: f32 = 1, + minion_throw_cooldown: Nanoseconds = 0, minion_carried_wood: ?EntityId = null, tree_health: u32 = 0, tree_max_health: u32 = 0, - station_subtype: StationSubtype = .chop, station_spawn_tree_cooldown: Nanoseconds = 0, station_owned_tree: ?EntityId = null, wood_carried_by: ?EntityId = null, + wood_burning: bool = false, + wood_health: f32 = 0, + wood_max_health: f32 = 0, pos: Vec2 = .init(0, 0), vel: Vec2 = .init(0, 0), @@ -184,23 +188,25 @@ const Entity = struct { animation_timer: Nanoseconds = 0, const MinionState = enum { + wander, + goto_tree_station, chop_tree, + pickup_wood, - burn + + goto_burn_station, + burn_wood }; const Type = enum { nil, minion, - station, + wood_station, + burn_station, tree, wood, - }; - - const StationSubtype = enum { - chop, - burn + eternal_flame, }; const Flags = packed struct { @@ -264,22 +270,35 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .entities = .init(&self.entities_buffer), .prng = Prng.init(@truncate(@as(u96, @intCast(now.nanoseconds)))), .camera_pos = .init(0, 0), - .font = font + .font = font, + .eternal_flame_id = undefined, }; self.spawnMinion(.init(0, 0)); // try self.spawnMinion(.init(32, 0)); - self.spawnStation(-100, .chop); - self.spawnStation(-150, .chop); - self.spawnStation(100, .burn); - self.spawnStation(150, .burn); + self.spawnWoodStation(-100); + self.spawnWoodStation(-150); + self.spawnBurnStation(100); + self.spawnBurnStation(150); + + self.eternal_flame_id = self.spawnEntity(Entity{ + .type = .eternal_flame, + .pos = .init(200, 0) + }).?; return null; } +fn spawnEntity(self: *App, e: Entity) ?EntityId { + return self.entities.insert(e) catch { + log.warn("Failed to spawn entity {}, entity limit reached", .{e.type}); + return null; + }; +} + pub fn spawnMinion(self: *App, pos: Vec2) void { - _ = self.entities.insert(Entity{ + _ = self.spawnEntity(Entity{ .type = .minion, .flags = .{ .has_animation = true, @@ -292,43 +311,42 @@ pub fn spawnMinion(self: *App, pos: Vec2) void { .max_walk_speed = 300, .animation = .idle, - }) catch { - log.warn("Failed to spawn minion, entity limit reached", .{}); - return; - }; + }); } -pub fn spawnStation(self: *App, pos: f32, subtype: Entity.StationSubtype) void { - _ = self.entities.insert(Entity{ - .type = .station, - .station_subtype = subtype, +pub fn spawnWoodStation(self: *App, pos: f32) void { + _ = self.spawnEntity(Entity{ + .type = .wood_station, .pos = .init(pos, 0), - }) catch { - log.warn("Failed to spawn station, entity limit reached", .{}); - return; - }; + }); +} + +pub fn spawnBurnStation(self: *App, pos: f32) void { + _ = self.spawnEntity(Entity{ + .type = .burn_station, + + .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); + assert(station.type == .wood_station); if (station.station_owned_tree != null) { return; } const health = 10; - const id = self.entities.insert(Entity{ + if (self.spawnEntity(Entity{ .type = .tree, .tree_health = health, .tree_max_health = health, .pos = station.pos - }) catch { - log.warn("Failed to spawn tree, entity limit reached", .{}); - return; - }; - station.station_owned_tree = id; + })) |id| { + station.station_owned_tree = id; + } } fn destroyTree(self: *App, tree_id: EntityId) void { @@ -341,12 +359,85 @@ fn destroyTree(self: *App, tree_id: EntityId) void { } fn spawnWood(self: *App, pos: Vec2) void { - _ = self.entities.insert(Entity{ + const health = 20; + + _ = self.spawnEntity(Entity{ .type = .wood, - .pos = pos - }) catch { - log.warn("Failed to spawn wood, entity limit reached", .{}); - return; + .pos = pos, + .wood_health = health, + .wood_max_health = health + }); +} + +const EntityIteratorByDistance = struct { + arena: Allocator, + entities: *EntitySlotMap, + used: std.ArrayList(EntityId), + + type: Entity.Type, + origin: Vec2, + + pub fn next(self: *EntityIteratorByDistance) !?EntityId { + var nearest: ?EntityId = null; + var nearest_distance: f32 = 0; + + var entity_iter = self.entities.iterator(); + while (entity_iter.next()) |entity_id| { + const entity = self.entities.getAssumeExists(entity_id); + if (entity.type != self.type) { + continue; + } + + const distance = entity.pos.distanceSqr(self.origin); + if (nearest == null or nearest_distance > distance) { + if (std.mem.indexOfScalar(EntityId, self.used.items, entity_id) != null) { + continue; + } + + nearest = entity_id; + nearest_distance = distance; + } + } + + if (nearest != null) { + try self.used.append(self.arena, nearest.?); + } + + return nearest; + } +}; + +const EntityIteratorByType = struct { + entities: *EntitySlotMap, + iter: EntitySlotMap.Iterator, + type: Entity.Type, + + pub fn next(self: *EntityIteratorByType) ?EntityId { + while (self.iter.next()) |id| { + const entity = self.entities.getAssumeExists(id); + if (entity.type == self.type) { + return id; + } + } + return null; + } +}; + +fn initNearestEntityIter(self: *App, entity_type: Entity.Type, origin: Vec2, arena: Allocator) EntityIteratorByDistance { + return EntityIteratorByDistance{ + .arena = arena, + .entities = &self.entities, + .used = .empty, + .origin = origin, + .type = entity_type, + }; +} + +fn initEntityIteratorByType(self: *App, entity_type: Entity.Type) EntityIteratorByType { + return EntityIteratorByType{ + .entities = &self.entities, + .iter = self.entities.iterator(), + .type = entity_type }; } @@ -435,137 +526,123 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { const minion = self.entities.getAssumeExists(minion_id); assert(minion.type == .minion); - const goal_distance_threshold = 10; + const goal_distance = 10; var walk_goal: ?Vec2 = null; - var station_goal: ?Entity.StationSubtype = null; - if (minion.minion_state == .goto_tree_station) { - // if (minion.minion_carried_wood == null) { - // if (self.getNearestFreeEntity(minion.pos, .tree)) |tree_id| { - // _ = tree_id; // autofix - // } - // } - } + self.ensureEntityExists(&minion.minion_carried_wood); + minion.minion_chop_cooldown = @max(minion.minion_chop_cooldown - plt.dt, 0); switch (minion.minion_state) { + .wander => { + }, .goto_tree_station => { - // TODO: Steal tree if it is nearer to you compared to the current minion that has it reserved. - if (self.getNearestFreeEntity(minion.pos, .tree)) |tree_id| { - self.setEntityTarget(minion_id, tree_id); - minion.minion_state = .chop_tree; + if (minion.target_entity) |tree_station_id| { + const tree_station = self.entities.getAssumeExists(tree_station_id); + walk_goal = tree_station.pos; } 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); - } - } + minion.minion_state = .wander; } }, .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.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; } if (tree.tree_health <= 0) { - minion.minion_state = .burn; - self.unsetEntityTarget(minion_id); self.destroyTree(tree_id); + minion.minion_state = .wander; } } else { walk_goal = tree.pos; } - } - - if (!is_at_tree) { - minion.minion_work_progress = 0; + } else { + minion.minion_state = .wander; } }, .pickup_wood => { - }, - .burn => { - station_goal = .burn; + if (minion.target_entity) |wood_id| { + const tree = self.entities.getAssumeExists(wood_id); - var is_at_station = false; - 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; + if (minion.pos.distance(tree.pos) < goal_distance) { + self.unsetEntityTarget(minion_id); + self.minionPickupWood(minion_id, wood_id); + minion.minion_state = .wander; + } else { + walk_goal = tree.pos; } - } - - if (!is_at_station) { - minion.minion_work_progress = 0; - } - - if (is_at_station) { - minion.minion_work_progress += minion.minion_burn_speed * plt.deltaTime(); - } - - 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; + } else { + minion.minion_state = .wander; } }, - } - - if (station_goal) |station_type| { - 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) { + .goto_burn_station => { + if (minion.minion_carried_wood == null) { self.unsetEntityTarget(minion_id); + minion.minion_state = .wander; } - } - if (minion.target_entity == null) { - if (self.getNearestFreeStation(minion.pos, station_type)) |station_id| { - self.setEntityTarget(minion_id, station_id); + if (minion.target_entity) |burn_station_id| { + const burn_station = self.entities.getAssumeExists(burn_station_id); + if (minion.pos.distance(burn_station.pos) < goal_distance) { + minion.minion_state = .burn_wood; + } else { + walk_goal = burn_station.pos; + } + } else { + minion.minion_state = .wander; } - } + }, + .burn_wood => { + 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 (minion.target_entity) |station_id| { - const station = self.entities.getAssumeExists(station_id); - walk_goal = station.pos; - } + 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; + } else { + minion.minion_throw_cooldown = @max(minion.minion_throw_cooldown - plt.dt, 0); + if (minion.minion_throw_cooldown == 0) { + minion.minion_state = .wander; + } + } + }, } if (walk_goal) |pos| { const diff = minion.pos.x - pos.x; - if (diff < -goal_distance_threshold) { + if (diff < -goal_distance) { minion.acc = .init(minion.max_walk_speed, 0); - } else if (diff > goal_distance_threshold) { + } else if (diff > goal_distance) { minion.acc = .init(-minion.max_walk_speed, 0); } } } +fn minionPickupWood(self: *App, minion_id: EntityId, wood_id: EntityId) void { + const minion = self.entities.get(minion_id) orelse return; + assert(minion.type == .minion); + + const wood = self.entities.get(wood_id) orelse return; + assert(wood.type == .wood); + + wood.wood_carried_by = minion_id; + minion.minion_carried_wood = wood_id; +} + +fn ensureEntityExists(self: *App, maybe_entity_id: *?EntityId) void { + if (maybe_entity_id.* != null and !self.entities.exists(maybe_entity_id.*.?)) { + maybe_entity_id.* = null; + } +} + pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; _ = input; // autofix @@ -589,93 +666,214 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { Gfx.setClearColor(.rgb(40, 40, 40)); Gfx.drawLine(.init(-800, 0), .init(800, 0), .white, 5); - var station_iter = self.entities.iterator(); - while (station_iter.next()) |station_id| { - const station = self.entities.getAssumeExists(station_id); - if (station.type != .station) { - continue; - } + { // Spawn trees at wood station on a cooldown + var wood_station_iter = self.initEntityIteratorByType(.wood_station); + while (wood_station_iter.next()) |wood_station_id| { + const wood_station = self.entities.getAssumeExists(wood_station_id); - 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 (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; + } } - } - 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) { - self.spawnTree(station_id); + wood_station.station_spawn_tree_cooldown = @max(wood_station.station_spawn_tree_cooldown - plt.dt, 0); + if (wood_station.station_spawn_tree_cooldown == 0 and wood_station.station_owned_tree == null) { + self.spawnTree(wood_station_id); } } } - const character_size = Vec2.init(96, 96); - - { + { // 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| { const e = self.entities.getAssumeExists(entity_id); + self.ensureEntityExists(&e.target_entity); + self.ensureEntityExists(&e.targeted_by_entity); + 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 { + const other = self.entities.getAssumeExists(other_id); + if (other.targeted_by_entity != entity_id) { 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 { + const other = self.entities.getAssumeExists(other_id); + if (other.target_entity != entity_id) { e.targeted_by_entity = null; } } } } - 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; - } - } - { - var tree_iter = self.entities.iterator(); - while (tree_iter.next()) |id| { - const e = self.entities.getAssumeExists(id); + const burn_speed = 10; - 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)); + var wood_iter = self.initEntityIteratorByType(.wood); + while (wood_iter.next()) |wood_id| { + const wood = self.entities.getAssumeExists(wood_id); - 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)); + self.ensureEntityExists(&wood.wood_carried_by); + + // Apply burn + if (wood.wood_burning) { + wood.wood_health = @max(wood.wood_health - burn_speed * dt, 0); + + if (wood.wood_health == 0) { + self.heat += 1; + self.entities.removeAssumeExists(wood_id); + } + + continue; + } + + // Each wood entity will assign itself to the nearest minion that isn't busy + if (wood.wood_carried_by == null and (wood.targeted_by_entity == null or wood.wood_carried_by == null)) { + var minion_iter = self.initNearestEntityIter(.minion, wood.pos, plt.arena); + while (try minion_iter.next()) |minion_id| { + const minion = self.entities.getAssumeExists(minion_id); + if (minion.minion_carried_wood != null) { + continue; + } + + const state = minion.minion_state; + if (state == .goto_tree_station or state == .wander) { + self.setEntityTarget(minion_id, wood_id); + minion.minion_state = .pickup_wood; + 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| { + const tree = self.entities.getAssumeExists(tree_id); + if (tree.targeted_by_entity != null) { + continue; + } + + var minion_iter = self.initNearestEntityIter(.minion, tree.pos, plt.arena); + while (try minion_iter.next()) |minion_id| { + const minion = self.entities.getAssumeExists(minion_id); + if (minion.minion_carried_wood != null) { + continue; + } + + const state = minion.minion_state; + if (state == .wander or state == .goto_tree_station) { + self.setEntityTarget(minion_id, tree_id); + minion.minion_state = .chop_tree; + break; + } + } + } + } + + { // Each wood station will assign itself to the nearest minion that isn't busy + var wood_station_iter = self.initEntityIteratorByType(.wood_station); + while (wood_station_iter.next()) |wood_station_id| { + const wood_station = self.entities.getAssumeExists(wood_station_id); + if (wood_station.targeted_by_entity != null) { + continue; + } + + var minion_iter = self.initNearestEntityIter(.minion, wood_station.pos, plt.arena); + while (try minion_iter.next()) |minion_id| { + const minion = self.entities.getAssumeExists(minion_id); + if (minion.minion_carried_wood != null) { + continue; + } + + const state = minion.minion_state; + if (state == .wander) { + self.setEntityTarget(minion_id, wood_station_id); + minion.minion_state = .goto_tree_station; + break; + } + } + } + } + + { // 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); + if (minion.minion_carried_wood == null) { + continue; + } + + const state = minion.minion_state; + if (state == .wander) { + self.setEntityTarget(minion_id, burn_station_id); + minion.minion_state = .goto_burn_station; + break; + } + } + } + } + + { + 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; + } + } + } + + { + var wood_iter = self.initEntityIteratorByType(.wood); + while (wood_iter.next()) |wood_id| { + const wood = self.entities.getAssumeExists(wood_id); + + if (wood.wood_carried_by) |minion_id| { + const minion = self.entities.getAssumeExists(minion_id); + wood.pos = minion.pos.add(.init(0, -50)); + } + } + } + + { + var tree_iter = self.initEntityIteratorByType(.tree); + while (tree_iter.next()) |id| { + const e = self.entities.getAssumeExists(id); + + 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)); + } + } + + const character_size = Vec2.init(96, 96); { var entity_iter = self.entities.iterator(); while (entity_iter.next()) |id| { @@ -735,18 +933,15 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { ); Gfx.drawSprite(sprite, pos, size, .white); - 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)); + 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 == .station) { - if (e.station_subtype == .chop) { - Gfx.drawRectangle(e.pos.sub(.init(5, 5)), .init(10, 10), .rgb(20, 200, 20)); - } else { - Gfx.drawRectangle(e.pos.sub(.init(5, 5)), .init(10, 10), .rgb(200, 20, 20)); - } + if (e.type == .wood_station) { + Gfx.drawRectangle(e.pos.sub(.init(5, 5)), .init(10, 10), .rgb(20, 200, 20)); + } else if (e.type == .burn_station) { + 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)), @@ -757,13 +952,19 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } - { - var wood_iter = self.entities.iterator(); + { // 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 wood + var wood_iter = self.initEntityIteratorByType(.wood); while (wood_iter.next()) |id| { const wood = self.entities.getAssumeExists(id); - if (wood.type != .wood) { - continue; - } Gfx.drawRectangle(wood.pos.sub(.init(10, 10)), .init(20, 20), .rgb(100, 100, 20)); }