423 lines
12 KiB
Zig
423 lines
12 KiB
Zig
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);
|
|
}
|