From 7b99f3344cf9c65278fbaa126f1087f084fa227d Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 3 Oct 2026 17:55:50 +0300 Subject: [PATCH] add goals with colors --- assets/tiled/map.tmx | 14 +-- assets/tiled/tileset.tsx | 40 ++++++++ src/app.zig | 213 +++++++++++++++++++++++++++------------ 3 files changed, 197 insertions(+), 70 deletions(-) diff --git a/assets/tiled/map.tmx b/assets/tiled/map.tmx index bea0985..6ce0160 100644 --- a/assets/tiled/map.tmx +++ b/assets/tiled/map.tmx @@ -239,7 +239,7 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,98,98,98,98,98,98,0,0,0,0,0,0,0,0, 0,0,98,0,0,0,0,98,98,98,98,98,98,0,0,0, -0,0,98,0,0,0,0,0,0,0,0,0,98,98,0,0 +0,0,98,0,0,8,42,0,12,0,0,0,98,98,0,0 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -332,7 +332,7 @@ 0,0,0,99,0,98,98,0,0,0,0,0,0,0,0,0 -0,0,98,0,72,0,7,0,0,0,0,40,0,98,0,0, +0,0,98,0,72,0,7,7,0,0,0,40,0,98,0,0, 0,0,98,0,0,0,0,0,0,0,0,0,98,98,0,0, 0,0,98,98,98,98,98,98,98,98,98,98,98,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -350,7 +350,7 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 -0,0,98,0,72,0,0,7,0,0,0,75,0,98,0,0, +0,0,98,0,72,0,0,7,0,0,0,40,0,98,0,0, 0,0,98,0,0,0,0,7,0,0,0,0,98,98,0,0, 0,0,98,98,98,98,98,98,98,98,98,98,98,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -368,7 +368,7 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 -0,98,0,72,0,0,7,0,0,0,75,0,98,0,0,98, +0,98,0,72,0,0,7,0,0,0,40,0,98,0,0,98, 0,98,0,0,0,0,7,0,0,99,0,98,98,0,0,98, 0,98,98,98,98,98,98,98,98,98,98,98,0,0,0,98, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -386,7 +386,7 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 -0,72,0,0,0,0,0,12,75,0,98,0,0,98,0,72, +0,72,0,0,0,0,0,12,40,0,98,0,0,98,0,72, 0,0,0,0,0,0,0,99,0,98,98,0,0,98,0,0, 98,98,98,98,98,98,98,98,98,98,0,0,0,98,98,98, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -404,7 +404,7 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 -0,7,7,0,12,12,75,0,98,0,0,98,0,72,0,0, +0,7,7,0,12,12,40,0,98,0,0,98,0,72,0,0, 0,0,0,0,99,99,0,98,98,0,0,98,0,0,0,0, 98,98,98,98,98,98,98,98,0,0,0,98,98,98,98,98, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, @@ -422,7 +422,7 @@ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 -0,0,0,12,75,0,98,0,0,0,0,0,0,0,0,0, +0,0,0,12,40,0,98,0,0,0,0,0,0,0,0,0, 7,0,0,99,0,98,98,0,0,0,0,0,0,0,0,0, 98,98,98,98,98,98,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, diff --git a/assets/tiled/tileset.tsx b/assets/tiled/tileset.tsx index 2267c09..5b3d03e 100644 --- a/assets/tiled/tileset.tsx +++ b/assets/tiled/tileset.tsx @@ -3,26 +3,31 @@ + + + + + @@ -31,11 +36,23 @@ + + + + + + + + + + + + @@ -48,34 +65,51 @@ + + + + + + + + + + + + + + + + + @@ -91,6 +125,12 @@ + + + + + + diff --git a/src/app.zig b/src/app.zig index d81e85c..137cb3f 100644 --- a/src/app.zig +++ b/src/app.zig @@ -21,6 +21,23 @@ const Tilesheet = @import("./tilesheet.zig"); const App = @This(); +const BoxColor = enum { + none, + brown, + red, + blue, + green, + gray, + + var string_mapping = std.StaticStringMap(BoxColor).initComptime(.{ + .{ "brown", .brown }, + .{ "red", .red }, + .{ "blue", .blue }, + .{ "green", .green }, + .{ "gray", .gray }, + }); +}; + const Direction = enum { up, down, @@ -71,6 +88,7 @@ const Entity = struct { pos: Point = .{ .x = -1, .y = -1 }, box_hole_id: Entity.Id = .nil, + color: BoxColor = .none, const Type = enum { nil, @@ -139,26 +157,28 @@ const UndoFrame = struct { }; const World = struct { + entities_buffer: [256]Entity.SlotMap.Slot, entities: Entity.SlotMap, width: u32, height: u32, + level_number: i32, - pub fn clone(self: World, gpa: Allocator) !World { - var entity_slots = try self.entities.slots.clone(gpa); - errdefer entity_slots.deinit(gpa); - - var entities = self.entities; - entities.slots = entity_slots; - - return World{ - .width = self.width, - .height = self.height, - .entities = entities, + pub fn init(self: *World) void { + self.* = World{ + .entities_buffer = undefined, + .entities = .initBuffer(&self.entities_buffer), + .width = 0, + .height = 0, + .level_number = 0 }; } - pub fn deinit(self: *World, gpa: Allocator) void { - self.entities.deinit(gpa); + pub fn copy(self: *World, other: World) void { + self.* = other; + + const self_slots = self.entities_buffer[0..other.entities.slots.items.len]; + self.entities.slots.items = self_slots; + @memcpy(self_slots, other.entities.slots.items); } fn spawnEntity(self: *World, opts: Entity) Entity.Id { @@ -301,21 +321,28 @@ const TickContext = struct { } }; -const Level = struct { - number: i32, - world: World, +const BoxSprites = struct { + regular: Gfx.Sprite.Id, + in_hole: Gfx.Sprite.Id, + darker: Gfx.Sprite.Id, + + pub const nil = BoxSprites{ + .regular = .nil, + .in_hole = .nil, + .darker = .nil, + }; }; world: World, undo_stack: std.ArrayList(UndoFrame) = .empty, current_level: u32 = 0, -levels: std.ArrayList(Level), +levels: std.ArrayList(World), roboto_font: Font, tilesheet: Tilesheet, player_sprites: PlayerSprites, -hole_with_box_sprite: Gfx.Sprite.Id, +box_sprites_by_color: std.EnumArray(BoxColor, BoxSprites), animation_timer: Platform.Nanoseconds, footstep_buffer: Audio.BufferId, @@ -386,19 +413,46 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .animation_timer = 0, .tilesheet = tilesheet, .footstep_buffer = footstep_sound, - .hole_with_box_sprite = tilesheet.get(6, 4), + .box_sprites_by_color = .init(.{ + .none = .nil, + .brown = .{ + .regular = tilesheet.get(6, 0), + .darker = tilesheet.get(6, 1), + .in_hole = tilesheet.get(6, 4), + }, + .red = .{ + .regular = tilesheet.get(7, 0), + .darker = tilesheet.get(7, 1), + .in_hole = tilesheet.get(7, 4), + }, + .blue = .{ + .regular = tilesheet.get(8, 0), + .darker = tilesheet.get(8, 1), + .in_hole = tilesheet.get(8, 4), + }, + .green = .{ + .regular = tilesheet.get(9, 0), + .darker = tilesheet.get(9, 1), + .in_hole = tilesheet.get(9, 4), + }, + .gray = .{ + .regular = tilesheet.get(10, 0), + .darker = tilesheet.get(10, 1), + .in_hole = tilesheet.get(10, 4), + } + }), .sfx_bus = Audio.addBus(.{ .label = "sfx" }), .world = undefined, .levels = .empty, }; self.levels = try self.loadLevels(plt.gpa, plt.assets); - self.world = try self.levels.items[0].world.clone(plt.gpa); + self.world.copy(self.levels.items[0]); return null; } -fn loadLevels(self: *App, gpa: Allocator, assets: *Platform.Assets) !std.ArrayList(Level) { +fn loadLevels(self: *App, gpa: Allocator, assets: *Platform.Assets) !std.ArrayList(World) { var xml_parser = try Tiled.xml.Parser.initCapacity(gpa, .{}); defer xml_parser.deinit(gpa); @@ -418,9 +472,11 @@ fn loadLevels(self: *App, gpa: Allocator, assets: *Platform.Assets) !std.ArrayLi .root )); - var levels: std.ArrayList(Level) = .empty; + var levels: std.ArrayList(World) = .empty; const level_bounds = tilemap.getLayer("Level bounds").?; + try levels.ensureTotalCapacity(gpa, level_bounds.variant.object.items.len); + for (level_bounds.variant.object.items) |obj| { if (obj.shape != .rectangle) { continue; @@ -445,37 +501,32 @@ fn loadLevels(self: *App, gpa: Allocator, assets: *Platform.Assets) !std.ArrayLi }; level_bound = tilemap.trimBoundsByEmptyTiles(level_bound); - var level = try self.loadLevel(gpa, tilemap, tilesets, level_bound); - level.number = level_index; - try levels.append(gpa, level); + var level = levels.addOneAssumeCapacity(); + try self.loadLevel(level, tilemap, tilesets, level_bound); + level.level_number = level_index; } const Context = struct { - fn lessThan(_: @This(), lhs: Level, rhs: Level) bool { - return lhs.number < rhs.number; + fn lessThan(_: @This(), lhs: World, rhs: World) bool { + return lhs.level_number < rhs.level_number; } }; - std.mem.sort(Level, levels.items, Context{}, Context.lessThan); + std.mem.sort(World, levels.items, Context{}, Context.lessThan); return levels; } fn loadLevel( self: *App, - gpa: Allocator, + world: *World, tilemap: Tiled.Tilemap, tilesets: Tiled.Tileset.List, bounds: Tiled.Tilemap.Bounds -) !Level { - var level = Level{ - .number = 0, - .world = .{ - .width = bounds.width(), - .height = bounds.height(), - .entities = try .initCapacity(gpa, 256) - } - }; +) !void { + world.init(); + world.width = bounds.width(); + world.height = bounds.height(); const ground = tilemap.getLayer("Ground").?; const foreground = tilemap.getLayer("Foreground").?; @@ -487,7 +538,7 @@ fn loadLevel( if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| { const pos = tiled_tile.getPosition(); - _ = level.world.spawnEntity(Entity{ + _ = world.spawnEntity(Entity{ .type = .nil, .visual_layer = .ground, .pos = .{ @@ -502,29 +553,34 @@ fn loadLevel( const pos = tiled_tile.getPosition(); const props = tiled_tile.getProperties(); const props_type = props.getString("type") orelse ""; + const props_color = props.getString("color") orelse ""; var entity_type: ?Entity.Type = null; var layer: Entity.Layer = .surface; + var color: BoxColor = .none; if (std.mem.eql(u8, props_type, "wall")) { entity_type = .wall; } else if (std.mem.eql(u8, props_type, "box")) { entity_type = .box; + color = BoxColor.string_mapping.get(props_color) orelse return error.UnknownBoxColor; } else if (std.mem.eql(u8, props_type, "player")) { entity_type = .player; } else if (std.mem.eql(u8, props_type, "coin")) { entity_type = .coin; } else if (std.mem.eql(u8, props_type, "box_hole")) { entity_type = .box_hole; + color = BoxColor.string_mapping.get(props_color) orelse .none; layer = .on_ground; } else if (std.mem.eql(u8, props_type, "box_goal")) { entity_type = .box_goal; layer = .on_ground; + color = BoxColor.string_mapping.get(props_color) orelse .none; } else { log.warn("Unknown tile type: '{s}' at {} {}", .{props_type, x, y}); } if (entity_type != null) { - _ = level.world.spawnEntity(Entity{ + _ = world.spawnEntity(Entity{ .type = entity_type.?, .visual_layer = layer, .pos = .{ @@ -532,25 +588,26 @@ fn loadLevel( .y = @intCast(oy), }, .visual_pos = .initFromInt(usize, ox, oy), - .sprite = self.tilesheet.get(pos.x, pos.y) + .sprite = self.tilesheet.get(pos.x, pos.y), + .color = color }); } } } } - - return level; } const PlayerInput = struct { move_dir: Point, - undo: bool + undo: bool, + restart: bool }; fn getPlayerInput(input: *Platform.Input) PlayerInput { var result: PlayerInput = .{ .move_dir = .{ .x = 0, .y = 0 }, - .undo = false + .undo = false, + .restart = false }; if (input.isKeyPressedOrRepeat(.S)) { @@ -570,6 +627,7 @@ fn getPlayerInput(input: *Platform.Input) PlayerInput { } result.undo = input.isKeyPressedOrRepeat(.Z); + result.restart = input.isKeyPressed(.R); return result; } @@ -596,12 +654,14 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } + if (input.restart) { + try self.setWorldByLevel(self.current_level); + } + var tick_context: TickContext = .{ .world = &self.world }; - var goal_count: u32 = 0; - var box_at_goal_count: u32 = 0; { var entity_iter = self.world.entities.iterator(); while (entity_iter.next()) |entity_id| { @@ -620,12 +680,6 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .bus = self.sfx_bus }); } - } else if (entity.type == .box_goal) { - goal_count += 1; - - if (self.world.entityExists(.{ .pos = entity.pos, .type = .box })) { - box_at_goal_count += 1; - } } else if (entity.type == .box) { interpolate_pos = true; } @@ -662,6 +716,28 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { try self.undo_stack.append(plt.gpa, tick_context.undo_frame); } + var goal_count: u32 = 0; + var box_at_goal_count: u32 = 0; + { + var goal_iter = self.world.iterEntities(.{ .type = .box_goal }); + while (goal_iter.next()) |goal_id| { + const goal = self.world.entities.getAssumeExists(goal_id); + goal_count += 1; + + if (self.world.findEntity(.{ .pos = goal.pos, .type = .box })) |box| { + var has_correct_color = true; + if (goal.color != .none) { + has_correct_color = goal.color == box.color; + } + + const is_on_top = box.visual_pos.distance(goal.visual_pos) < 0.05; + if (is_on_top and has_correct_color) { + box_at_goal_count += 1; + } + } + } + } + inline for (.{ Entity.Layer.ground, Entity.Layer.on_ground, Entity.Layer.surface }) |layer| { var entity_iter = self.world.entities.iterator(); while (entity_iter.next()) |entity_id| { @@ -672,7 +748,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { var sprite = entity.sprite; - const box_hole_margin = 0.2; + const box_hole_margin = 0.1; if (entity.type == .box) { const box_hole_id = self.world.findEntityId(.{ .type = .box_hole, .pos = entity.pos }); if (self.world.entities.get(box_hole_id)) |box_hole| { @@ -680,12 +756,25 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { sprite = null; } } + + if (self.world.findEntity(.{ .type = .box_goal, .pos = entity.pos })) |box_goal| { + if (box_goal.visual_pos.distance(entity.visual_pos) < 0.2) { + var is_correct = true; + if (box_goal.color != .none) { + is_correct = box_goal.color == entity.color; + } + + if (is_correct) { + sprite = self.box_sprites_by_color.get(entity.color).darker; + } + } + } } else if (entity.type == .box_hole) { var box_iter = self.world.iterEntities(.{ .type = .box, .pos = entity.pos }); while (box_iter.next()) |box_id| { const box = self.world.entities.getAssumeExists(box_id); if (box.visual_pos.distance(entity.visual_pos) < box_hole_margin) { - sprite = self.hole_with_box_sprite; + sprite = self.box_sprites_by_color.get(box.color).in_hole; } } } @@ -703,21 +792,19 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { if (goal_count > 0 and goal_count == box_at_goal_count and self.current_level+1 < self.levels.items.len) { self.current_level += 1; - self.world.deinit(plt.gpa); - self.world = try self.levels.items[self.current_level].world.clone(plt.gpa); - self.undo_stack.clearRetainingCapacity(); + try self.setWorldByLevel(self.current_level); } } +pub fn setWorldByLevel(self: *App, index: u32) !void { + self.world.copy(self.levels.items[index]); + self.undo_stack.clearRetainingCapacity(); +} + pub fn deinit(self: *App, plt: Platform.Deinit) void { self.roboto_font.deinit(plt.gpa); Gfx.deinitSpritesheet(self.roboto_font.spritesheet); - self.world.deinit(plt.gpa); - - for (self.levels.items) |*level| { - level.world.deinit(plt.gpa); - } self.levels.deinit(plt.gpa); self.undo_stack.deinit(plt.gpa); }