const std = @import("std"); const log = std.log.scoped(.app); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const STBImage = @import("stb_image"); const STBTrueType = @import("stb_truetype"); const Tiled = @import("tiled"); const Platform = @import("platform/root.zig"); const Gfx = Platform.Gfx; const Vec2 = Platform.Vec2; const Audio = Platform.Audio; const ImGUI = Platform.ImGUI; const Rect = Platform.Rect; const Color = Platform.Color; const CanvasScaler = @import("./canvas_scaler.zig"); const Font = @import("./font.zig"); const Tilesheet = @import("./tilesheet.zig"); const App = @This(); const Direction = enum { up, down, left, right }; const PlayerSprites = struct { up: [3]Gfx.Sprite.Id, down: [3]Gfx.Sprite.Id, left: [3]Gfx.Sprite.Id, right: [3]Gfx.Sprite.Id, pub fn getDirection(self: *PlayerSprites, dir: Direction) []Gfx.Sprite.Id { return switch (dir) { .up => &self.up, .down => &self.down, .left => &self.left, .right => &self.right, }; } }; const Tile = struct { ground_sprite: ?Gfx.Sprite.Id, entities_by_layer: std.EnumArray(Entity.Layer, Entity.Id), const empty = Tile{ .ground_sprite = null, .entities_by_layer = .initFill(.nil) }; }; const Tilemap = struct { tiles: []Tile, width: usize, height: usize, pub fn init(gpa: Allocator, width: usize, height: usize) !Tilemap { const tiles = try gpa.alloc(Tile, width * height); @memset(tiles, .empty); return Tilemap{ .width = width, .height = height, .tiles = tiles }; } pub fn clone(self: Tilemap, gpa: Allocator) !Tilemap { const tiles = try gpa.dupe(Tile, self.tiles); errdefer gpa.free(tiles); return Tilemap{ .tiles = tiles, .width = self.width, .height = self.height, }; } pub fn deinit(self: *Tilemap, gpa: Allocator) void { gpa.free(self.tiles); } pub fn get(self: *Tilemap, x: usize, y: usize) *Tile { assert(x < self.width); assert(y < self.height); return &self.tiles[y * self.width + x]; } }; const Entity = struct { type: Type, layer: Layer, pos_x: i32 = -1, pos_y: i32 = -1, visual_pos: Vec2 = .init(0, 0), sprite: ?Gfx.Sprite.Id = null, player_direction: Direction = .down, player_frame_index: u32 = 0, const Type = enum { player, box, goal_marker, wall }; const Layer = enum { ground, surface, }; const SlotMap = Platform.SlotMapType(u16, u8, Entity); const Id = SlotMap.Id; }; const World = struct { tilemap: Tilemap, entities: Entity.SlotMap, pub fn clone(self: World, gpa: Allocator) !World { var tilemap = try self.tilemap.clone(gpa); errdefer tilemap.deinit(gpa); var entity_slots = try self.entities.slots.clone(gpa); errdefer entity_slots.deinit(gpa); var entities = self.entities; entities.slots = entity_slots; return World{ .tilemap = tilemap, .entities = entities }; } pub fn deinit(self: *World, gpa: Allocator) void { self.tilemap.deinit(gpa); self.entities.deinit(gpa); } fn spawnEntity(self: *World, opts: Entity) Entity.Id { var e = opts; e.pos_x = -1; e.pos_y = -1; const entity_id = self.entities.insert(e) catch |err| { log.err("Failed to spawn entity: {}", .{err}); return .nil; }; if (opts.pos_x != -1 or opts.pos_y != -1) { assert(opts.pos_x != -1 and opts.pos_y != -1); if (!self.setEntityPosition(entity_id, opts.pos_x, opts.pos_y)) { log.err("Entity position: {}, {}", .{opts.pos_x, opts.pos_y}); @panic("Failed to set initial entity position"); } } return entity_id; } fn getTile(self: *World, x: i32, y: i32) ?*Tile { const width: i32 = @intCast(self.tilemap.width); const height: i32 = @intCast(self.tilemap.height); if (0 <= x and x < width and 0 <= y and y < height) { return &self.tilemap.tiles[@intCast(x + y * width)]; } return null; } fn setEntityPosition(self: *World, id: Entity.Id, x: i32, y: i32) bool { const e = self.entities.get(id) orelse return false; const destination_tile = self.getTile(x, y) orelse return false; if (self.entities.exists(destination_tile.entities_by_layer.get(e.layer))) { // Cant't overwrite entity return false; } destination_tile.entities_by_layer.set(e.layer, id); if (self.getTile(e.pos_x, e.pos_y)) |tile| { tile.entities_by_layer.set(e.layer, .nil); } e.pos_x = x; e.pos_y = y; return true; } fn moveEntity(self: *World, id: Entity.Id, dx: i32, dy: i32) bool { const e = self.entities.get(id) orelse return false; const next_x = e.pos_x+dx; const next_y = e.pos_y+dy; const destination_tile = self.getTile(next_x, next_y) orelse return false; if (e.type == .player or e.type == .box) { const destination_entity_id = destination_tile.entities_by_layer.get(e.layer); if (self.entities.get(destination_entity_id)) |destination_entity| { if (destination_entity.type == .box) { if (!self.moveEntity(destination_entity_id, dx, dy)) { return false; } } } } return self.setEntityPosition(id, next_x, next_y); } }; const Level = struct { number: i32, world: World, }; world: World, current_level: u32 = 0, levels: std.ArrayList(Level), roboto_font: Font, tilesheet: Tilesheet, player_sprites: PlayerSprites, animation_timer: Platform.Nanoseconds, footstep_buffer: Audio.BufferId, footstep_sound: Audio.SoundId = .nil, sfx_bus: Audio.BusId, pub fn init(self: *App, plt: Platform.Init) !?u8 { const roboto_font = try Font.init(plt.gpa, .{ .spritesheet = Gfx.initSpritesheet(.{ .format = .r8 }), .ttf_data = plt.assets.readFile("roboto-font/Roboto-Regular.ttf") }); const tilesheet1_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png")); const tilesheet2_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet@2.png")); assert(tilesheet2_png.width == tilesheet1_png.width*2); assert(tilesheet2_png.height == tilesheet1_png.height*2); var levels = [_][*]u8{ tilesheet2_png.rgba8_pixels, tilesheet1_png.rgba8_pixels }; const tilesheet = try Tilesheet.init( plt.arena, .init(.{ .width = tilesheet2_png.width, .height = tilesheet2_png.height, .format = .rgba8, .levels = &levels, }), .init(128, 128), .{ .padding = 1, .spritesheet = Gfx.initSpritesheet(.{ .mipmaps = levels.len }) } ); var player_sprites: PlayerSprites = undefined; inline for (.{ .{ .dir = .up, .positions = [_]Vec2{ .init(3, 4), .init(4, 4), .init(5, 4) } }, .{ .dir = .down, .positions = [_]Vec2{ .init(0, 4), .init(1, 4), .init(2, 4) } }, .{ .dir = .right, .positions = [_]Vec2{ .init(0, 6), .init(1, 6), .init(2, 6) }, }, .{ .dir = .left, .positions = [_]Vec2{ .init(3, 6), .init(4, 6), .init(5, 6) } } }) |entry| { var sprite_ids = player_sprites.getDirection(entry.dir); inline for (0.., entry.positions) |i, pos| { sprite_ids[i] = tilesheet.get(@intFromFloat(pos.x), @intFromFloat(pos.y)); } } const footstep_sound = Audio.addBuffer(.{ .vorbis = plt.assets.readFile("impact-sounds/footstep_concrete_000.ogg") }); self.* = App{ .player_sprites = player_sprites, .roboto_font = roboto_font, .animation_timer = 0, .tilesheet = tilesheet, .footstep_buffer = footstep_sound, .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); return null; } fn loadLevels(self: *App, gpa: Allocator, assets: *Platform.Assets) !std.ArrayList(Level) { var xml_parser = try Tiled.xml.Parser.initCapacity(gpa, .{}); defer xml_parser.deinit(gpa); const tilemap = try Tiled.Tilemap.initFromXml( gpa, try xml_parser.parse(assets.readFile("tiled/map.tmx")), .root ); defer tilemap.deinit(); var tilesets: Tiled.Tileset.List = .empty; defer tilesets.deinit(gpa); try tilesets.add(gpa, "tileset.tsx", try Tiled.Tileset.initFromXml( gpa, try xml_parser.parse(assets.readFile("tiled/tileset.tsx")), .root )); var levels: std.ArrayList(Level) = .empty; const level_bounds = tilemap.getLayer("Level bounds").?; for (level_bounds.variant.object.items) |obj| { if (obj.shape != .rectangle) { continue; } const level_index = obj.properties.getInt("level") orelse { log.warn("Level bounds doesn't have index", .{}); continue; }; var rect = obj.shape.rectangle; rect.x /= @floatFromInt(tilemap.tile_width); rect.y /= @floatFromInt(tilemap.tile_height); rect.width /= @floatFromInt(tilemap.tile_width); rect.height /= @floatFromInt(tilemap.tile_height); var level_bound = Tiled.Tilemap.Bounds{ .min_x = @intFromFloat(@floor(rect.x)), .min_y = @intFromFloat(@floor(rect.y)), .max_x = @intFromFloat(@ceil(rect.x + rect.width)), .max_y = @intFromFloat(@ceil(rect.y + rect.height)), }; 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); } const Context = struct { fn lessThan(_: @This(), lhs: Level, rhs: Level) bool { return lhs.number < rhs.number; } }; std.mem.sort(Level, levels.items, Context{}, Context.lessThan); return levels; } fn loadLevel( self: *App, gpa: Allocator, tilemap: Tiled.Tilemap, tilesets: Tiled.Tileset.List, bounds: Tiled.Tilemap.Bounds ) !Level { var level = Level{ .number = 0, .world = .{ .tilemap = try .init(gpa, bounds.width(), bounds.height()), .entities = try .initCapacity(gpa, 256) } }; const ground = tilemap.getLayer("Ground").?; const foreground = tilemap.getLayer("Foreground").?; for (0..bounds.height()) |oy| { for (0..bounds.width()) |ox| { const x = bounds.min_x + @as(i32, @intCast(ox)); const y = bounds.min_y + @as(i32, @intCast(oy)); const tile = level.world.tilemap.get(ox, oy); if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| { const pos = tiled_tile.getPosition(); tile.ground_sprite = self.tilesheet.get(pos.x, pos.y); } if (tilemap.getTile(foreground, tilesets, x, y)) |tiled_tile| { const pos = tiled_tile.getPosition(); const props = tiled_tile.getProperties(); const props_type = props.getString("type") orelse ""; var entity_type: ?Entity.Type = null; var layer: Entity.Layer = .surface; if (std.mem.eql(u8, props_type, "wall")) { entity_type = .wall; } else if (std.mem.eql(u8, props_type, "box")) { entity_type = .box; } else if (std.mem.eql(u8, props_type, "goal_marker")) { entity_type = .goal_marker; layer = .ground; } else { log.warn("Unknown tile type: '{s}'", .{props_type}); } if (entity_type != null) { _ = level.world.spawnEntity(Entity{ .type = entity_type.?, .layer = layer, .pos_x = @intCast(ox), .pos_y = @intCast(oy), .sprite = self.tilesheet.get(pos.x, pos.y) }); } } } } for (tilemap.getLayer("Markers").?.variant.object.items) |obj| { if (obj.shape != .point) { continue; } if (!std.mem.eql(u8, obj.name, "spawn")) { continue; } var position = obj.shape.point; position.x /= @floatFromInt(tilemap.tile_width); position.y /= @floatFromInt(tilemap.tile_height); const pos_x: i32 = @intFromFloat(position.x); const pos_y: i32 = @intFromFloat(position.y); if (bounds.isInside(pos_x, pos_y)) { _ = level.world.spawnEntity(Entity{ .type = .player, .layer = .surface, .pos_x = pos_x - bounds.min_x, .pos_y = pos_y - bounds.min_y }); } } return level; } fn updateWorld(world: *World) void { _ = world; // autofix } pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; var dir_y: i32 = 0; var dir_x: i32 = 0; if (input.isKeyPressedOrRepeat(.S)) { dir_y += 1; } if (input.isKeyPressedOrRepeat(.W)) { dir_y -= 1; } if (input.isKeyPressedOrRepeat(.D)) { dir_x += 1; } if (input.isKeyPressedOrRepeat(.A)) { dir_x -= 1; } if (dir_x != 0) { dir_y = 0; } const clear_color = Color.rgb(40, 40, 40); Gfx.setClearColor(clear_color); Gfx.setDefaultSampler(.linear); const tile_size = Vec2.init(48, 48); var canvas = CanvasScaler{ .size = Vec2.initFromInt(usize, self.world.tilemap.width, self.world.tilemap.height).multiply(tile_size) }; canvas.push(); defer canvas.pop(clear_color); 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| { const entity = self.world.entities.getAssumeExists(entity_id); if (entity.type == .player) { if ((dir_x != 0 or dir_y != 0) and self.world.moveEntity(entity_id, dir_x, dir_y)) { var new_direction = entity.player_direction; if (dir_x < 0) { new_direction = .left; } else if (dir_x > 0) { new_direction = .right; } else if (dir_y > 0) { new_direction = .down; } else if (dir_y < 0) { new_direction = .up; } if (entity.player_direction != new_direction) { entity.player_frame_index = 0; entity.player_direction = new_direction; } self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .bus = self.sfx_bus }); } const frames = self.player_sprites.getDirection(entity.player_direction); if (dir_x != 0 or dir_y != 0) { self.animation_timer += plt.input.delta_time; const animatin_time = std.time.ns_per_s / 5; while (self.animation_timer >= animatin_time) { entity.player_frame_index = (entity.player_frame_index + 1) % @as(u32, @intCast(frames.len)); self.animation_timer -= animatin_time; } } else { self.animation_timer = 0; entity.player_frame_index = 0; } entity.sprite = frames[entity.player_frame_index]; } else if (entity.type == .goal_marker) { goal_count += 1; const tile = self.world.getTile(entity.pos_x, entity.pos_y).?; if (self.world.entities.get(tile.entities_by_layer.get(.surface))) |above_entity| { if (above_entity.type == .box) { box_at_goal_count += 1; } } } entity.visual_pos = .initFromInt(i32, entity.pos_x, entity.pos_y); } } for (0..self.world.tilemap.height) |y| { for (0..self.world.tilemap.width) |x| { const tile = self.world.tilemap.get(x, y); if (tile.ground_sprite) |sprite| { Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white); } } } inline for (.{ Entity.Layer.ground, Entity.Layer.surface }) |layer| { 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) { continue; } if (entity.sprite) |sprite| { Gfx.drawSprite( sprite, entity.visual_pos.multiply(tile_size), tile_size, .white ); } } } 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); } } 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); }