From 930a7dc2755df532a7819e57f8626a836792f5b5 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 3 Oct 2026 14:56:31 +0300 Subject: [PATCH] add box hole --- README.md | 11 ++++ src/app.zig | 178 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 148 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 139e983..718ab22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ # Gaem +Plan: + +- Push boxes into goals +- Push boxes into a hole +- Collect optional coins +- Filtered goals and holes +- A set of levels +- Main menu +- Transitions between levels +- Particle effects + ## Run Linux and Windows: diff --git a/src/app.zig b/src/app.zig index 885ef8e..d81e85c 100644 --- a/src/app.zig +++ b/src/app.zig @@ -62,15 +62,16 @@ const Point = struct { const Entity = struct { type: Type, - layer: Layer, - pos: Point = .{ .x = -1, .y = -1 }, visual_pos: Vec2 = .init(0, 0), + visual_layer: Layer, sprite: ?Gfx.Sprite.Id = null, - player_direction: Direction = .down, player_frame_index: u32 = 0, + pos: Point = .{ .x = -1, .y = -1 }, + box_hole_id: Entity.Id = .nil, + const Type = enum { nil, @@ -97,6 +98,7 @@ const UndoFrame = struct { id: Entity.Id, pos: Point, player_direction: Direction, + box_hole_id: Entity.Id, }; entities_buffer: [8]EntityState, @@ -125,6 +127,15 @@ const UndoFrame = struct { pub fn isEmpty(self: UndoFrame) bool { return self.entities_len == 0; } + + pub fn apply(self: *const UndoFrame, world: *World) void { + for (self.entities()) |e_state| { + var e = world.entities.getAssumeExists(e_state.id); + e.pos = e_state.pos; + e.player_direction = e_state.player_direction; + e.box_hole_id = e_state.box_hole_id; + } + } }; const World = struct { @@ -157,16 +168,66 @@ const World = struct { }; } - fn findEntity(self: *World, pos: Point, layer: Entity.Layer) Entity.Id { - var entity_iter = self.entities.iterator(); - while (entity_iter.next()) |entity_id| { + const Iterator = struct { + iter: Entity.SlotMap.Iterator, + opts: Options, + + const Options = struct { + pos: ?Point = null, + type: ?Entity.Type = null + }; + + pub fn next(self: *Iterator) ?Entity.Id { + while (self.iter.next()) |id| { + const e = self.iter.slot_map.getAssumeExists(id); + if (self.opts.type) |e_type| { + if (e.type != e_type) { + continue; + } + } + + if (self.opts.pos) |pos| { + if (!e.pos.eql(pos)) { + continue; + } + } + + return id; + } + + return null; + } + }; + + fn iterEntities(self: *World, opts: Iterator.Options) Iterator { + return Iterator{ + .iter = self.entities.iterator(), + .opts = opts + }; + } + + fn findEntityId(self: *World, opts: Iterator.Options) Entity.Id { + var iter = self.iterEntities(opts); + return iter.next() orelse .nil; + } + + fn findEntity(self: *World, opts: Iterator.Options) ?*Entity { + return self.entities.get(self.findEntityId(opts)); + } + + fn entityExists(self: *World, opts: Iterator.Options) bool { + return self.entities.exists(self.findEntityId(opts)); + } + + pub fn hasBoxInHole(self: *World, hole_id: Entity.Id) bool { + var iter = self.entities.iterator(); + while (iter.next()) |entity_id| { const entity = self.entities.getAssumeExists(entity_id); - if (entity.layer == layer and entity.pos.eql(pos)) { - return entity_id; + if (entity.box_hole_id == hole_id) { + return true; } } - - return .nil; + return false; } }; @@ -179,45 +240,62 @@ const TickContext = struct { return false; } - const e = self.world.entities.get(id) orelse return false; + const this = self.world.entities.get(id) orelse return false; - const next_pos = e.pos.add(dir); + const next_pos = this.pos.add(dir); + var box_hole_id = this.box_hole_id; - if (e.type == .player or e.type == .box) { - const destination_entity_id = self.world.findEntity(next_pos, e.layer); - if (self.world.entities.get(destination_entity_id)) |destination_entity| { - if (destination_entity.type == .box) { - if (!self.moveEntity(destination_entity_id, dir)) { + var iter = self.world.iterEntities(.{ .pos = next_pos }); + while (iter.next()) |other_id| { + const other = self.world.entities.getAssumeExists(other_id); + if (other.type == .wall) { + return false; + } + if (other.type == .box_hole) { + if (!self.world.hasBoxInHole(other_id)) { + if (this.type == .box and box_hole_id == Entity.Id.nil) { + box_hole_id = other_id; + } else { return false; } } - else if (destination_entity.type == .wall) { - return false; + } + if (other.type == .box) { + if (other.box_hole_id == Entity.Id.nil) { + if (this.type == .player) { + if (!self.moveEntity(other_id, dir)) { + return false; + } + } else { + return false; + } } } } if (!self.undo_frame.pushEntity(.{ .id = id, - .pos = e.pos, - .player_direction = e.player_direction + .pos = this.pos, + .player_direction = this.player_direction, + .box_hole_id = this.box_hole_id })) { log.warn("Failed to push entity undo", .{}); } - if (e.type == .player) { + if (this.type == .player) { if (dir.x < 0) { - e.player_direction = .left; + this.player_direction = .left; } else if (dir.x > 0) { - e.player_direction = .right; + this.player_direction = .right; } else if (dir.y > 0) { - e.player_direction = .down; + this.player_direction = .down; } else if (dir.y < 0) { - e.player_direction = .up; + this.player_direction = .up; } } - e.pos = next_pos; + this.pos = next_pos; + this.box_hole_id = box_hole_id; return true; } @@ -237,6 +315,7 @@ roboto_font: Font, tilesheet: Tilesheet, player_sprites: PlayerSprites, +hole_with_box_sprite: Gfx.Sprite.Id, animation_timer: Platform.Nanoseconds, footstep_buffer: Audio.BufferId, @@ -307,6 +386,7 @@ 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), .sfx_bus = Audio.addBus(.{ .label = "sfx" }), .world = undefined, .levels = .empty, @@ -409,7 +489,7 @@ fn loadLevel( _ = level.world.spawnEntity(Entity{ .type = .nil, - .layer = .ground, + .visual_layer = .ground, .pos = .{ .x = @intCast(ox), .y = @intCast(oy), @@ -435,6 +515,7 @@ fn loadLevel( entity_type = .coin; } else if (std.mem.eql(u8, props_type, "box_hole")) { entity_type = .box_hole; + layer = .on_ground; } else if (std.mem.eql(u8, props_type, "box_goal")) { entity_type = .box_goal; layer = .on_ground; @@ -445,7 +526,7 @@ fn loadLevel( if (entity_type != null) { _ = level.world.spawnEntity(Entity{ .type = entity_type.?, - .layer = layer, + .visual_layer = layer, .pos = .{ .x = @intCast(ox), .y = @intCast(oy), @@ -511,11 +592,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { if (input.undo) { if (self.undo_stack.pop()) |undo_frame| { - for (undo_frame.entities()) |e_state| { - var e = self.world.entities.getAssumeExists(e_state.id); - e.pos = e_state.pos; - e.player_direction = e_state.player_direction; - } + undo_frame.apply(&self.world); } } @@ -546,10 +623,8 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } else if (entity.type == .box_goal) { goal_count += 1; - if (self.world.entities.get(self.world.findEntity(entity.pos, .surface))) |above_entity| { - if (above_entity.type == .box) { - box_at_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; @@ -591,13 +666,33 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { var entity_iter = self.world.entities.iterator(); while (entity_iter.next()) |entity_id| { const entity = self.world.entities.getAssumeExists(entity_id); - if (entity.layer != layer) { + if (entity.visual_layer != layer) { continue; } - if (entity.sprite) |sprite| { + var sprite = entity.sprite; + + const box_hole_margin = 0.2; + 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| { + if (entity.box_hole_id == box_hole_id and box_hole.visual_pos.distance(entity.visual_pos) < box_hole_margin) { + sprite = null; + } + } + } 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; + } + } + } + + if (sprite != null) { Gfx.drawSprite( - sprite, + sprite.?, entity.visual_pos.multiply(tile_size), tile_size, .white @@ -610,6 +705,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { 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(); } }