kble/src/level.zig

228 lines
6.8 KiB
Zig

// SPDX-License-Identifier: MIT
// Copyright (c) 2021 KikooDX
// This file is part of [KBLE](https://sr.ht/~kikoodx/kble), which is
// MIT licensed. The MIT license requires this copyright notice to be
// included in all copies and substantial portions of the software.
//! Level structure, grid containing tile data.
const ray = @cImport({
@cInclude("raylib.h");
});
const std = @import("std");
const expect = std.testing.expect;
const Vec2 = @import("vec2.zig");
const Rect = @import("rect.zig");
const SelectionUpdate = @import("movement.zig").SelectionUpdate;
const Self = @This();
pub const cell_type = u16;
width: u16,
height: u16,
content: []cell_type,
selection: []bool,
/// Create structure and allocate required memory. The `content` array size will
/// be equal to `width` times `height`.
pub fn init(allocator: *std.mem.Allocator, width: u16, height: u16) !Self {
var self = Self{
.width = width,
.height = height,
.content = undefined,
.selection = undefined,
};
// Try to allocate necessary memory.
const size: u32 = @intCast(u32, width) * @intCast(u32, height);
self.content = try allocator.alloc(cell_type, size);
errdefer allocator.free(self.content);
self.selection = try allocator.alloc(bool, size);
// Fill with default value to avoid undefined behavior.
var i: u32 = 0;
while (i < size) : (i += 1) {
self.content[i] = 0;
self.selection[i] = false;
}
return self;
}
/// Load level content from given absolute path. Expect the KBLE file format (see
/// `kbleformat.md` for more details.
pub fn init_load(allocator: *std.mem.Allocator, kble_file_path: []const u8) !Self {
var self = Self{
.width = undefined,
.height = undefined,
.content = undefined,
.selection = undefined,
};
// Open directory.
var dir: std.fs.Dir = std.fs.cwd();
// Open file in read mode.
const file: std.fs.File = try dir.openFile(kble_file_path, std.fs.File.OpenFlags{
.read = true,
.write = false,
});
defer file.close();
return self;
}
/// Free the level content.
pub fn deinit(self: *Self, allocator: *std.mem.Allocator) void {
allocator.free(self.selection);
allocator.free(self.content);
}
/// Draw level tiles from `offset` to fill the window.
pub fn draw(self: *Self, scale: u16, offset: Vec2) void {
// Pixel position (were we draw).
var x: Vec2.int_type = 0;
var y: Vec2.int_type = 0;
// Cursor position.
var cx: Vec2.int_type = offset.x;
while (cx < self.width) {
var cy: Vec2.int_type = offset.y;
while (cy < self.height) {
const cell_content: cell_type = self.content[cy * self.width + cx];
const color = switch (cell_content) {
0 => ray.Color{ .r = 26, .g = 26, .b = 26, .a = 255 },
1 => ray.Color{ .r = 144, .g = 144, .b = 144, .a = 255 },
else => ray.PURPLE, //unknown
};
ray.DrawRectangle(x + 1, y + 1, scale - 2, scale - 2, color);
y += scale;
cy += 1;
}
y = 0;
x += scale;
cx += 1;
}
}
/// Draw selection from `offset` to fill the window.
pub fn draw_selection(self: *Self, scale: u16, offset: Vec2) void {
// Pixel position (were we draw).
var x: Vec2.int_type = 0;
var y: Vec2.int_type = 0;
// Cursor position.
var cx: Vec2.int_type = offset.x;
while (cx < self.width) {
var cy: Vec2.int_type = offset.y;
while (cy < self.height) {
if (self.selection[cy * self.width + cx])
ray.DrawRectangleLines(x, y, scale, scale, ray.WHITE);
y += scale;
cy += 1;
}
y = 0;
x += scale;
cx += 1;
}
}
/// Set the `state` of a cell at `cursor` if possible
pub fn select_cell(self: *Self, cursor: Vec2, state: bool) void {
const target: u32 = @intCast(u32, cursor.y) * self.width + @intCast(u32, cursor.x);
if (cursor.x < self.width and cursor.y < self.height and
target < self.width * self.height and target >= 0)
{
self.selection[target] = state;
}
}
/// Reset state of selection: `state` everywhere.
fn select_clear(self: *Self, state: bool) void {
const size: u32 = @intCast(u32, self.width) * @intCast(u32, self.height);
var i: u32 = 0;
while (i < size) : (i += 1) {
self.selection[i] = state;
}
}
/// Change state of all the cells in the rectangle range.
pub fn select_rect(self: *Self, rect: Rect, state: bool) void {
var cx: Rect.int_type = rect.left_x;
while (cx <= rect.right_x) {
defer cx += 1;
var cy = rect.top_y;
while (cy <= rect.bottom_y) {
defer cy += 1;
self.select_cell(Vec2{ .x = cx, .y = cy }, state);
}
}
}
/// Apply selection update to selection *kof*.
pub fn apply_selection_update(self: *Self, selection_update: SelectionUpdate) void {
// Apply changes.
self.select_rect(selection_update.area, selection_update.state);
}
test "create level buffer" {
// Create allocator.
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = &arena.allocator;
// Initialize level struct and allocate space (twice 'cause why not?).
var level: Self = try Self.init(allocator, 64, 32);
level.deinit(allocator);
level = try Self.init(allocator, 256, 256);
defer level.deinit(allocator);
level.content[128] = 32;
level.selection[128] = true;
expect(level.width == 256);
expect(level.height == 256);
expect(level.content[128] == 32);
expect(level.selection[128]);
}
test "clear selection" {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = &arena.allocator;
var level: Self = try Self.init(allocator, 16, 16);
defer level.deinit(allocator);
level.selection[255] = true;
level.select_clear(false);
expect(!level.selection[255]);
}
test "select rectangle area" {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = &arena.allocator;
var level: Self = try Self.init(allocator, 16, 16);
defer level.deinit(allocator);
level.select_rect(Rect{
.left_x = 4,
.right_x = 20,
.top_y = 1,
.bottom_y = 14,
}, true);
expect(!level.selection[14]);
expect(!level.selection[19]);
expect(level.selection[20]);
expect(level.selection[31]);
expect(!level.selection[245]);
}
test "load level from existing file" {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = &arena.allocator;
var level: Self = try Self.init_load(allocator, "sample.kble");
//defer level.deinit(allocator);
}