add box hole

This commit is contained in:
Rokas Puzonas 2026-10-03 14:56:31 +03:00
parent 01c6d334f7
commit 930a7dc275
2 changed files with 148 additions and 41 deletions

View File

@ -1,5 +1,16 @@
# Gaem # 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 ## Run
Linux and Windows: Linux and Windows:

View File

@ -62,15 +62,16 @@ const Point = struct {
const Entity = struct { const Entity = struct {
type: Type, type: Type,
layer: Layer,
pos: Point = .{ .x = -1, .y = -1 },
visual_pos: Vec2 = .init(0, 0), visual_pos: Vec2 = .init(0, 0),
visual_layer: Layer,
sprite: ?Gfx.Sprite.Id = null, sprite: ?Gfx.Sprite.Id = null,
player_direction: Direction = .down, player_direction: Direction = .down,
player_frame_index: u32 = 0, player_frame_index: u32 = 0,
pos: Point = .{ .x = -1, .y = -1 },
box_hole_id: Entity.Id = .nil,
const Type = enum { const Type = enum {
nil, nil,
@ -97,6 +98,7 @@ const UndoFrame = struct {
id: Entity.Id, id: Entity.Id,
pos: Point, pos: Point,
player_direction: Direction, player_direction: Direction,
box_hole_id: Entity.Id,
}; };
entities_buffer: [8]EntityState, entities_buffer: [8]EntityState,
@ -125,6 +127,15 @@ const UndoFrame = struct {
pub fn isEmpty(self: UndoFrame) bool { pub fn isEmpty(self: UndoFrame) bool {
return self.entities_len == 0; 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 { const World = struct {
@ -157,16 +168,66 @@ const World = struct {
}; };
} }
fn findEntity(self: *World, pos: Point, layer: Entity.Layer) Entity.Id { const Iterator = struct {
var entity_iter = self.entities.iterator(); iter: Entity.SlotMap.Iterator,
while (entity_iter.next()) |entity_id| { opts: Options,
const entity = self.entities.getAssumeExists(entity_id);
if (entity.layer == layer and entity.pos.eql(pos)) { const Options = struct {
return entity_id; 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;
} }
} }
return .nil; 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.box_hole_id == hole_id) {
return true;
}
}
return false;
} }
}; };
@ -179,45 +240,62 @@ const TickContext = struct {
return false; 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) { var iter = self.world.iterEntities(.{ .pos = next_pos });
const destination_entity_id = self.world.findEntity(next_pos, e.layer); while (iter.next()) |other_id| {
if (self.world.entities.get(destination_entity_id)) |destination_entity| { const other = self.world.entities.getAssumeExists(other_id);
if (destination_entity.type == .box) { if (other.type == .wall) {
if (!self.moveEntity(destination_entity_id, dir)) { 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; return false;
} }
} }
else if (destination_entity.type == .wall) { }
if (other.type == .box) {
if (other.box_hole_id == Entity.Id.nil) {
if (this.type == .player) {
if (!self.moveEntity(other_id, dir)) {
return false; return false;
} }
} else {
return false;
}
}
} }
} }
if (!self.undo_frame.pushEntity(.{ if (!self.undo_frame.pushEntity(.{
.id = id, .id = id,
.pos = e.pos, .pos = this.pos,
.player_direction = e.player_direction .player_direction = this.player_direction,
.box_hole_id = this.box_hole_id
})) { })) {
log.warn("Failed to push entity undo", .{}); log.warn("Failed to push entity undo", .{});
} }
if (e.type == .player) { if (this.type == .player) {
if (dir.x < 0) { if (dir.x < 0) {
e.player_direction = .left; this.player_direction = .left;
} else if (dir.x > 0) { } else if (dir.x > 0) {
e.player_direction = .right; this.player_direction = .right;
} else if (dir.y > 0) { } else if (dir.y > 0) {
e.player_direction = .down; this.player_direction = .down;
} else if (dir.y < 0) { } 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; return true;
} }
@ -237,6 +315,7 @@ roboto_font: Font,
tilesheet: Tilesheet, tilesheet: Tilesheet,
player_sprites: PlayerSprites, player_sprites: PlayerSprites,
hole_with_box_sprite: Gfx.Sprite.Id,
animation_timer: Platform.Nanoseconds, animation_timer: Platform.Nanoseconds,
footstep_buffer: Audio.BufferId, footstep_buffer: Audio.BufferId,
@ -307,6 +386,7 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.animation_timer = 0, .animation_timer = 0,
.tilesheet = tilesheet, .tilesheet = tilesheet,
.footstep_buffer = footstep_sound, .footstep_buffer = footstep_sound,
.hole_with_box_sprite = tilesheet.get(6, 4),
.sfx_bus = Audio.addBus(.{ .label = "sfx" }), .sfx_bus = Audio.addBus(.{ .label = "sfx" }),
.world = undefined, .world = undefined,
.levels = .empty, .levels = .empty,
@ -409,7 +489,7 @@ fn loadLevel(
_ = level.world.spawnEntity(Entity{ _ = level.world.spawnEntity(Entity{
.type = .nil, .type = .nil,
.layer = .ground, .visual_layer = .ground,
.pos = .{ .pos = .{
.x = @intCast(ox), .x = @intCast(ox),
.y = @intCast(oy), .y = @intCast(oy),
@ -435,6 +515,7 @@ fn loadLevel(
entity_type = .coin; entity_type = .coin;
} else if (std.mem.eql(u8, props_type, "box_hole")) { } else if (std.mem.eql(u8, props_type, "box_hole")) {
entity_type = .box_hole; entity_type = .box_hole;
layer = .on_ground;
} else if (std.mem.eql(u8, props_type, "box_goal")) { } else if (std.mem.eql(u8, props_type, "box_goal")) {
entity_type = .box_goal; entity_type = .box_goal;
layer = .on_ground; layer = .on_ground;
@ -445,7 +526,7 @@ fn loadLevel(
if (entity_type != null) { if (entity_type != null) {
_ = level.world.spawnEntity(Entity{ _ = level.world.spawnEntity(Entity{
.type = entity_type.?, .type = entity_type.?,
.layer = layer, .visual_layer = layer,
.pos = .{ .pos = .{
.x = @intCast(ox), .x = @intCast(ox),
.y = @intCast(oy), .y = @intCast(oy),
@ -511,11 +592,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
if (input.undo) { if (input.undo) {
if (self.undo_stack.pop()) |undo_frame| { if (self.undo_stack.pop()) |undo_frame| {
for (undo_frame.entities()) |e_state| { undo_frame.apply(&self.world);
var e = self.world.entities.getAssumeExists(e_state.id);
e.pos = e_state.pos;
e.player_direction = e_state.player_direction;
}
} }
} }
@ -546,11 +623,9 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
} else if (entity.type == .box_goal) { } else if (entity.type == .box_goal) {
goal_count += 1; goal_count += 1;
if (self.world.entities.get(self.world.findEntity(entity.pos, .surface))) |above_entity| { if (self.world.entityExists(.{ .pos = entity.pos, .type = .box })) {
if (above_entity.type == .box) {
box_at_goal_count += 1; box_at_goal_count += 1;
} }
}
} else if (entity.type == .box) { } else if (entity.type == .box) {
interpolate_pos = true; interpolate_pos = true;
} }
@ -591,13 +666,33 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
var entity_iter = self.world.entities.iterator(); var entity_iter = self.world.entities.iterator();
while (entity_iter.next()) |entity_id| { while (entity_iter.next()) |entity_id| {
const entity = self.world.entities.getAssumeExists(entity_id); const entity = self.world.entities.getAssumeExists(entity_id);
if (entity.layer != layer) { if (entity.visual_layer != layer) {
continue; 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( Gfx.drawSprite(
sprite, sprite.?,
entity.visual_pos.multiply(tile_size), entity.visual_pos.multiply(tile_size),
tile_size, tile_size,
.white .white
@ -610,6 +705,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
self.current_level += 1; self.current_level += 1;
self.world.deinit(plt.gpa); self.world.deinit(plt.gpa);
self.world = try self.levels.items[self.current_level].world.clone(plt.gpa); self.world = try self.levels.items[self.current_level].world.clone(plt.gpa);
self.undo_stack.clearRetainingCapacity();
} }
} }