607 lines
18 KiB
Zig
607 lines
18 KiB
Zig
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);
|
|
}
|