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
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:

View File

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