const std = @import("std"); const log = std.log.scoped(.app); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Math = @import("math"); const Vec2 = Math.Vec2; 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 Audio = Platform.Audio; const ImGUI = Platform.ImGUI; const Rect = Platform.Rect; const Color = Platform.Color; const CanvasScaler = @import("./canvas_scaler.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 Tilemap = struct { tiles: []Tile, width: usize, height: usize, const Tile = struct { ground_sprite: ?Gfx.Sprite.Id, wall_sprite: ?Gfx.Sprite.Id, const empty = Tile{ .ground_sprite = null, .wall_sprite = null }; }; 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 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, pos_x: i32 = 0, pos_y: i32 = 0, sprite: ?Gfx.Sprite.Id = null, player_direction: Direction = .down, player_frame_index: u32 = 0, const Type = enum { player, box }; const SlotMap = Platform.SlotMapType(u8, u16, Entity); const Id = SlotMap.Id; }; roboto_font: Gfx.Font, tilemap: Tilemap, entities: Entity.SlotMap, tilesheet: Tilesheet, player_sprites: PlayerSprites, animation_timer: Platform.Nanoseconds, footstep_buffer: Audio.BufferId, footstep_sound: Audio.SoundId = .nil, sfx_bus: Audio.BusId, const Tilesheet = struct { image: Gfx.ImageData, sprites: []Gfx.Sprite.Id, tile_size: Vec2, width: u32, height: u32, pub fn init(gpa: Allocator, image: Gfx.ImageData, tile_width: u32, tile_height: u32) !Tilesheet { const width = image.width / tile_width; const height = image.height / tile_height; const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height); errdefer gpa.free(sprites); @memset(sprites, .nil); return Tilesheet{ .sprites = sprites, .image = image, .width = width, .height = height, .tile_size = .initFromInt(u32, tile_width, tile_height) }; } pub fn deinit(self: Tilesheet) void { for (self.sprites) |sprite| { Gfx.deinitSprite(sprite); } } pub fn get(self: Tilesheet, x: usize, y: usize) Gfx.Sprite.Id { if (x >= self.width or y >= self.height) { log.warn("Attempt to get tile which is out of bounds", .{}); return .nil; } const sprite_index = y * self.width + x; if (self.sprites[sprite_index] == Gfx.Sprite.Id.nil) { const sprite = Gfx.initSprite(.{ .padding = 1 }); Gfx.setSprite(sprite, .{ .image = self.image, .rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size) }); self.sprites[sprite_index] = sprite; } return self.sprites[sprite_index]; } }; pub fn init(self: *App, plt: Platform.Init) !?u8 { const roboto_font = try Gfx.Font.init(plt.gpa, .{ .spritesheet = Gfx.initSpritesheet(.{ .format = .r8 }), .ttf_data = plt.assets.readFile("roboto-font/Roboto-Regular.ttf") }); const tilesheet_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png")); const tilesheet = try Tilesheet.init( plt.arena, Gfx.ImageData{ .width = tilesheet_png.width, .height = tilesheet_png.height, .pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels } }, 64, 64, ); 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" }), .tilemap = undefined, .entities = try .initCapacity(plt.gpa, 256) }; var player_pos_x: i32 = 0; var player_pos_y: i32 = 0; { var scratch = std.heap.ArenaAllocator.init(plt.gpa); defer scratch.deinit(); var buffers = Tiled.xml.Lexer.Buffers.init(plt.gpa); defer buffers.deinit(); const tilemap = try Tiled.Tilemap.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/map.tmx")); defer tilemap.deinit(); var tilesets: Tiled.Tileset.List = .empty; defer tilesets.deinit(plt.gpa); try tilesets.add(plt.gpa, "tileset.tsx", try Tiled.Tileset.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/tileset.tsx"))); const bounds = tilemap.getTileBounds(); self.tilemap = try Tilemap.init(plt.gpa, bounds.width(), bounds.height()); const ground = tilemap.getLayer("Ground").?; const walls = tilemap.getLayer("Walls").?; for (0..bounds.height()) |oy| { for (0..bounds.width()) |ox| { const x = bounds.min_x + ox; const y = bounds.min_y + oy; const tile = self.tilemap.get(ox, oy); if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| { const pos = tiled_tile.getPosition(); tile.ground_sprite = tilesheet.get(pos.x, pos.y); } if (tilemap.getTile(walls, tilesets, x, y)) |tiled_tile| { const pos = tiled_tile.getPosition(); tile.wall_sprite = tilesheet.get(pos.x, pos.y); } } } const markers = &tilemap.getLayer("Markers").?.variant.object; const spawn = markers.getByName("spawn").?; player_pos_x = @divFloor(@as(i32, @intFromFloat(spawn.shape.point.x)), @as(i32, @intCast(tilemap.tile_width))); player_pos_x -= @intCast(bounds.min_x); player_pos_y = @divFloor(@as(i32, @intFromFloat(spawn.shape.point.y)),@as(i32, @intCast(tilemap.tile_height))); player_pos_y -= @intCast(bounds.min_y); const boxes = tilemap.getLayer("Boxes").?; for (0..bounds.height()) |oy| { for (0..bounds.width()) |ox| { const x = bounds.min_x + ox; const y = bounds.min_y + oy; if (tilemap.getTile(boxes, tilesets, x, y)) |tiled_tile| { const pos = tiled_tile.getPosition(); _ = try self.spawnBox(@intCast(ox), @intCast(oy), tilesheet.get(pos.x, pos.y)); } } } } const player_id = try self.spawnPlayer(); const player = self.entities.getAssumeExists(player_id); player.pos_x = player_pos_x; player.pos_y = player_pos_y; return null; } fn spawnPlayer(self: *App) !Entity.Id { return try self.entities.insert(Entity{ .type = .player, }); } fn spawnBox(self: *App, x: i32, y: i32, sprite: Gfx.Sprite.Id) !Entity.Id { return try self.entities.insert(Entity{ .type = .box, .pos_x = x, .pos_y = y, .sprite = sprite }); } 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); const tile_size = Vec2.init(48, 48); var canvas = CanvasScaler{ .size = Vec2.initFromInt(usize, self.tilemap.width, self.tilemap.height).multiply(tile_size) }; canvas.push(); defer canvas.pop(clear_color); for (0..self.tilemap.height) |y| { for (0..self.tilemap.width) |x| { const tile = self.tilemap.get(x, y); if (tile.ground_sprite) |sprite| { Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white); } if (tile.wall_sprite) |sprite| { Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white); } } } var entity_iter = self.entities.iterator(); while (entity_iter.next()) |entity_id| { const entity = self.entities.getAssumeExists(entity_id); if (entity.type == .player) { entity.pos_x += dir_x; entity.pos_y += 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; } 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; } if (dir_x != 0 or dir_y != 0) { self.footstep_sound = Audio.play(.{ .buffer = self.footstep_buffer, .bus = self.sfx_bus }); } entity.sprite = frames[entity.player_frame_index]; } if (entity.sprite) |sprite| { Gfx.drawSprite( sprite, Vec2.initFromInt(i32, entity.pos_x, entity.pos_y).multiply(tile_size), tile_size, .white ); } } // self.roboto_font.drawText(.{ // .text = try std.fmt.allocPrint(plt.frame, "Hello {:.0}!", .{plt.time()}), // .pos = .init(0, 0), // .height = 32, // }); } pub fn deinit(self: *App, plt: Platform.Deinit) void { self.roboto_font.deinit(plt.gpa); Gfx.deinitSpritesheet(self.roboto_font.spritesheet); self.tilemap.deinit(plt.gpa); self.entities.slots.deinit(plt.gpa); }