Files
zig-a-puzzle-a-day/src/root.zig
T
simkir c29af146dd Scale up
And make work with colemak hehe
2026-03-13 12:53:36 +01:00

1137 lines
30 KiB
Zig

//! By convention, root.zig is the root source file when making a library.
const std = @import("std");
const builtin = @import("builtin");
pub const platform = @import("platform.zig");
const TILE_SIZE = 64;
const V2 = struct {
x: f32,
y: f32,
const max = V2{ .x = std.math.floatMax(f32), .y = std.math.floatMax(f32) };
const zero = V2{ .x = 0.0, .y = 0.0 };
fn init(x: f32, y: f32) V2 {
return .{
.x = x,
.y = y,
};
}
fn initI32(x: i32, y: i32) V2 {
return .{
.x = @floatFromInt(x),
.y = @floatFromInt(y),
};
}
fn initUSize(x: usize, y: usize) V2 {
return .{
.x = @floatFromInt(x),
.y = @floatFromInt(y),
};
}
fn add(self: V2, other: V2) V2 {
return .{
.x = self.x + other.x,
.y = self.y + other.y,
};
}
fn sub(self: V2, other: V2) V2 {
return .{
.x = self.x - other.x,
.y = self.y - other.y,
};
}
fn mul(self: V2, s: f32) V2 {
return .{
.x = self.x * s,
.y = self.y * s,
};
}
fn inner(self: V2, other: V2) f32 {
const result = self.x * other.x + self.y * other.y;
return result;
}
fn hadamard(self: V2, other: V2) V2 {
var result = V2.zero;
result.x = self.x * other.x;
result.y = self.y * other.y;
return result;
}
};
const Rect = struct {
pos: V2,
size: V2,
};
const BBox = struct {
min: V2,
max: V2,
center: V2,
};
const Camera = struct {
pos: V2,
offset: V2,
};
const Image = struct {
width: i32,
height: i32,
stride: i32,
components: i32,
data: []u8,
};
const BoardTile = struct {
blocked: bool,
taken: bool,
hovering: bool,
label: []const u8,
fn init(label: []const u8, blocked: bool) BoardTile {
const result: BoardTile = .{
.blocked = blocked,
.taken = false,
.hovering = false,
.label = label,
};
return result;
}
};
const Board = struct {
pos: V2,
width: u32,
height: u32,
tile_size: V2,
tiles: [7][7]BoardTile,
fn getTilePos(self: Board, i: usize, j: usize) V2 {
const tile_pos = V2{
.x = @as(f32, @floatFromInt(j)) * self.tile_size.x,
.y = @as(f32, @floatFromInt(i)) * self.tile_size.y,
};
const result = tile_pos.add(self.pos);
return result;
}
};
const BoardIndex = struct {
i: usize,
j: usize,
fn init(i: usize, j: usize) BoardIndex {
return .{
.i = i,
.j = j,
};
}
fn max() BoardIndex {
return .{
.i = std.math.maxInt(usize),
.j = std.math.maxInt(usize),
};
}
};
const BrickTile = struct {
id: u32,
board_index: ?BoardIndex,
};
const Brick = struct {
rotating: bool,
pos: V2,
color: u32,
rotating_time: f64,
board_indices: [4][4]?BoardIndex,
tiles: [4][4]u32,
fn transpose(self: Brick) [4][4]u32 {
var result = [4][4]u32{
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
};
for (0..self.tiles.len) |i| {
const row = self.tiles[i];
for (0..row.len) |j| {
result[i][j] = self.tiles[j][i];
}
}
return result;
}
fn rotate(self: Brick) [4][4]u32 {
var result = [4][4]u32{
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
};
for (0..self.tiles.len) |i| {
const row = self.tiles[i];
for (0..row.len) |j| {
result[j][row.len - i - 1] = self.tiles[i][j];
}
}
return result;
}
};
const State = struct {
frame_count: i32,
mouse_pos: V2,
camera: Camera,
font_image: Image,
draw_bounding_rects: bool,
grabbed_brick_index: ?usize,
bricks: [8]Brick,
board: Board,
};
const tile_size = V2{
.x = TILE_SIZE,
.y = TILE_SIZE,
};
fn getTilePos(i: usize, j: usize) V2 {
return V2{
.x = @floatFromInt(j * TILE_SIZE),
.y = @floatFromInt(i * TILE_SIZE),
};
}
fn pointInsideRect(p: V2, rect_start: V2, rect_size: V2) bool {
var result: bool = false;
const rect_end: V2 = rect_start.add(rect_size);
const inside_x = rect_start.x < p.x and p.x < rect_end.x;
const inside_y = rect_start.y < p.y and p.y < rect_end.y;
result = inside_x and inside_y;
return result;
}
fn tilesIntersects(r1: V2, r2: V2) bool {
var result = false;
const r2a: V2 = V2.init(r2.x, r2.y);
const r2b: V2 = V2.init(r2.x, r2.y + tile_size.y);
const r2c: V2 = V2.init(r2.x + tile_size.x, r2.y);
const r2d: V2 = V2.init(r2.x + tile_size.x, r2.y + tile_size.y);
const a = pointInsideRect(r2a, r1, tile_size);
const b = pointInsideRect(r2b, r1, tile_size);
const c = pointInsideRect(r2c, r1, tile_size);
const d = pointInsideRect(r2d, r1, tile_size);
result = a or b or c or d;
return result;
}
fn rectsIntersects(r1: V2, r1_size: V2, r2: V2, r2_size: V2) bool {
var result = false;
const r2a: V2 = V2.init(r2.x, r2.y);
const r2b: V2 = V2.init(r2.x, r2.y + r2_size.y);
const r2c: V2 = V2.init(r2.x + r2_size.x, r2.y);
const r2d: V2 = V2.init(r2.x + r2_size.x, r2.y + r2_size.y);
const a = pointInsideRect(r2a, r1, r1_size);
const b = pointInsideRect(r2b, r1, r1_size);
const c = pointInsideRect(r2c, r1, r1_size);
const d = pointInsideRect(r2d, r1, r1_size);
result = a or b or c or d;
return result;
}
fn brickHasValidPos(brick: *const Brick) bool {
var valid_pos = true;
for (brick.board_indices, 0..) |row, i| {
for (row, 0..) |opt, j| {
const tile = brick.tiles[i][j];
if (tile > 0) {
if (opt != null) {
valid_pos = valid_pos and true;
} else {
valid_pos = false;
}
}
}
}
return valid_pos;
}
fn findBrickBoundingBox(brick: *const Brick) BBox {
var result = BBox{ .min = V2.max, .max = V2.zero, .center = V2.zero };
for (brick.tiles, 0..) |row, i| {
for (row, 0..) |tile, j| {
if (tile > 0) {
const tile_start = getTilePos(i, j);
const tile_end = tile_start.add(tile_size);
const start = brick.pos.add(tile_start);
const end = brick.pos.add(tile_end);
if (start.x < result.min.x) result.min.x = start.x;
if (start.y < result.min.y) result.min.y = start.y;
if (result.max.x < end.x) result.max.x = end.x;
if (result.max.y < end.y) result.max.y = end.y;
}
}
}
result.center = .{
.x = (result.min.x + result.max.x) / 2.0,
.y = (result.min.y + result.max.y) / 2.0,
};
return result;
}
fn findBrickMinBoardIdx(brick: *const Brick) BoardIndex {
var result = BoardIndex.max();
for (brick.board_indices, 0..) |row, i| {
for (row, 0..) |opt, j| {
const tile = brick.tiles[i][j];
if (tile > 0) {
if (opt) |index| {
result.i = if (index.i < result.i) index.i else result.i;
result.j = if (index.j < result.j) index.j else result.j;
}
}
}
}
return result;
}
fn drawRect(buffer: platform.OffscreenBuffer, pos: V2, size: V2, color: u32, line_width: i32) void {
var pixels: []u32 = @ptrCast(@alignCast(buffer.data));
const start_x: i32 = if (pos.x < 0) 0 else @intFromFloat(pos.x);
const start_y: i32 = if (pos.y < 0) 0 else @intFromFloat(pos.y);
const end = pos.add(size);
const end_x: i32 = if (end.x < 0) 0 else @intFromFloat(end.x);
const end_y: i32 = if (end.y < 0) 0 else @intFromFloat(end.y);
const min_x: usize = @intCast(if (buffer.width < start_x) buffer.width else start_x);
const min_y: usize = @intCast(if (buffer.height < start_y) buffer.height else start_y);
const max_x: usize = @intCast(if (buffer.width < end_x) buffer.width else end_x);
const max_y: usize = @intCast(if (buffer.height < end_y) buffer.height else end_y);
for (min_y..max_y) |y| {
for (min_x..max_x) |x| {
const left = x < start_x + line_width;
const top = y < start_y + line_width;
const right = end_x - line_width - 1 < x;
const bottom = end_y - line_width - 1 < y;
if (left or top or right or bottom) {
const idx: usize = y * @as(usize, @intCast(buffer.width)) + x;
pixels[idx] = color;
}
}
}
}
fn fillRect(
buffer: platform.OffscreenBuffer,
pos_x: i32,
pos_y: i32,
width: i32,
height: i32,
color: u32,
) void {
var pixels: []u32 = @ptrCast(@alignCast(buffer.data));
var start_x: i32 = pos_x;
var start_y: i32 = pos_y;
var end_x: i32 = pos_x + width;
var end_y: i32 = pos_y + height;
if (start_x < 0) start_x = 0;
if (start_y < 0) start_y = 0;
if (buffer.width < start_x) start_x = buffer.width;
if (buffer.height < start_y) start_y = buffer.height;
if (end_x < 0) end_x = 0;
if (end_y < 0) end_y = 0;
if (buffer.width < end_x) end_x = buffer.width;
if (buffer.height < end_y) end_y = buffer.height;
for (@intCast(start_y)..@intCast(end_y)) |y| {
for (@intCast(start_x)..@intCast(end_x)) |x| {
const idx: usize = y * @as(usize, @intCast(buffer.width)) + x;
pixels[idx] = color;
}
}
}
fn fillSquare(
buffer: platform.OffscreenBuffer,
x: i32,
y: i32,
size: i32,
color: u32,
) void {
fillRect(buffer, x, y, size, size, color);
}
fn drawImage(buffer: platform.OffscreenBuffer, image: Image, pos: V2, scale: f32) void {
const pixels: []u32 = @ptrCast(@alignCast(buffer.data));
const scale_factor: f32 = 1.0 / scale;
const buffer_width: usize = @intCast(buffer.width);
const image_width: f32 = @floatFromInt(image.width);
const image_height: f32 = @floatFromInt(image.height);
var min_x: i32 = @intFromFloat(pos.x);
var min_y: i32 = @intFromFloat(pos.y);
var max_x: i32 = @intFromFloat(pos.x + scale * @as(f32, @floatFromInt(image.width)));
var max_y: i32 = @intFromFloat(pos.y + scale * @as(f32, @floatFromInt(image.height)));
var image_offset_x: i32 = 0;
var image_offset_y: i32 = 0;
if (min_x < 0) {
image_offset_x = -min_x;
min_x = 0;
}
if (min_y < 0) {
image_offset_y = -min_y;
min_y = 0;
}
if (buffer.width < max_x) max_x = buffer.width;
if (buffer.height < max_y) max_y = buffer.height;
const start_x: usize = @intCast(min_x);
const start_y: usize = @intCast(min_y);
const end_x: usize = @intCast(max_x);
const end_y: usize = @intCast(max_y);
var image_x: i32 = image_offset_x * image.components;
var image_y: i32 = image_offset_y;
for (start_y..end_y) |y| {
for (start_x..end_x) |x| {
const idx: usize = y * buffer_width + x;
const current: u32 = pixels[idx];
const current_alpha = (0xFF000000 & current) >> 24;
// const current_red = (0x00FF0000 & current) >> 16;
// const current_green = (0x0000FF00 & current) >> 8;
// const current_blue = (0x000000FF & current) >> 0;
const x_f: f32 = @as(f32, @floatFromInt(image_x)) / @as(f32, @floatFromInt(image.width));
const xff: f32 = scale_factor * x_f;
const new_x: i32 = @as(i32, @intFromFloat(image_width * xff));
const y_f: f32 = @as(f32, @floatFromInt(image_y)) / @as(f32, @floatFromInt(image.height));
const yff: f32 = scale_factor * y_f;
const new_y: i32 = @as(i32, @intFromFloat(image_height * yff));
const image_idx: usize = @intCast(new_y * image.stride + new_x);
const image_pixel: [*]u8 = image.data.ptr + image_idx;
var image_alpha: u32 = 0xFF;
const image_red: u32 = image_pixel[0];
const image_green: u32 = image_pixel[1];
const image_blue: u32 = image_pixel[2];
if (image.components > 3) {
image_alpha = image_pixel[3];
}
if (image.components == 3 and image_red == 0 and image_green == 0 and image_blue == 0) {
image_alpha = 0;
}
// u32 red = lerp_color(image_alpha, current_red, image_red);
// u32 green = lerp_color(image_alpha, current_green, image_green);
// u32 blue = lerp_color(image_alpha, current_blue, image_blue);
const red = image_red;
const green = image_green;
const blue = image_blue;
pixels[idx] = (current_alpha << 24) | (red << 16) | (green << 8) | blue;
image_x += image.components;
}
image_x = image_offset_x * image.components;
image_y += 1;
}
}
fn indexFont(font: Image, c: u8) V2 {
var result = V2.zero;
_ = font;
var i: i32 = 0;
var j: i32 = 0;
switch (c) {
' ' => {
i = 15;
j = 1;
},
'(' => {
i = 7;
j = 2;
},
')' => {
i = 8;
j = 2;
},
',' => {
i = 11;
j = 2;
},
'-' => {
i = 12;
j = 2;
},
'.' => {
i = 13;
j = 2;
},
'0' => {
i = 15;
j = 2;
},
'1' => {
i = 0;
j = 3;
},
'2' => {
i = 1;
j = 3;
},
'3' => {
i = 2;
j = 3;
},
'4' => {
i = 3;
j = 3;
},
'5' => {
i = 4;
j = 3;
},
'6' => {
i = 5;
j = 3;
},
'7' => {
i = 6;
j = 3;
},
'8' => {
i = 7;
j = 3;
},
'9' => {
i = 8;
j = 3;
},
':' => {
i = 9;
j = 3;
},
'V' => {
i = 5;
j = 5;
},
'a' => {
i = 1;
j = 6;
},
'b' => {
i = 2;
j = 6;
},
'c' => {
i = 3;
j = 6;
},
'd' => {
i = 4;
j = 6;
},
'e' => {
i = 5;
j = 6;
},
'f' => {
i = 6;
j = 6;
},
'g' => {
i = 7;
j = 6;
},
'h' => {
i = 8;
j = 6;
},
'i' => {
i = 9;
j = 6;
},
'j' => {
i = 10;
j = 6;
},
'k' => {
i = 11;
j = 6;
},
'l' => {
i = 12;
j = 6;
},
'm' => {
i = 13;
j = 6;
},
'n' => {
i = 14;
j = 6;
},
'o' => {
i = 15;
j = 6;
},
'p' => {
i = 0;
j = 7;
},
'q' => {
i = 1;
j = 7;
},
'r' => {
i = 2;
j = 7;
},
's' => {
i = 3;
j = 7;
},
't' => {
i = 4;
j = 7;
},
'u' => {
i = 5;
j = 7;
},
'v' => {
i = 6;
j = 7;
},
'w' => {
i = 7;
j = 7;
},
'x' => {
i = 8;
j = 7;
},
'y' => {
i = 9;
j = 7;
},
'z' => {
i = 10;
j = 7;
},
else => {
},
}
result.x = @floatFromInt(i * 32);
result.y = @floatFromInt(j * 32);
return result;
}
fn drawString(buffer: platform.OffscreenBuffer, image: Image, pos: V2, str: []const u8) void {
var current_pos = pos;
const font_size = 32;
const scale = 0.5;
const font_width_scaled = scale * font_size;
for (str) |c| {
const char_image_pos = indexFont(image, c);
const pos_x: i32 = @intFromFloat(char_image_pos.x);
const pos_y: i32 = @intFromFloat(char_image_pos.y);
const idx: usize = @intCast(pos_y * image.stride + (pos_x * image.components));
const ptr = image.data.ptr + idx;
const size: usize = @intCast(font_size * image.stride);
const char_image: Image = .{
.width = font_size,
.height = font_size,
.stride = image.stride,
.components = image.components,
.data = ptr[0..size],
};
drawImage(buffer, char_image, current_pos, scale);
current_pos.x += font_width_scaled;
}
}
fn keyPressed(key: *platform.Key) bool {
var result = false;
if (key.down) {
if (key.frame_count == 0) {
result = true;
}
key.frame_count += 1;
} else {
key.frame_count = 0;
}
return result;
}
fn loadPng(image_path: []const u8) ?Image {
var x: c_int = undefined;
var y: c_int = undefined;
var n: c_int = undefined;
const image_data: [*c]platform.c.stbi_uc = platform.c.stbi_load(@ptrCast(image_path), &x, &y, &n, 0);
if (image_data != 0) {
const stride = n * x;
const size: usize = @intCast(y * stride);
const ptr: []u8 = image_data[0..size];
return .{
.width = x,
.height = y,
.stride = stride,
.components = n,
.data = ptr,
};
} else {
return null;
}
}
pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBuffer, input: *platform.AppInput) void {
var state: *State = @ptrCast(@alignCast(memory.permanent_storage));
const mid = V2{
.x = @floatFromInt(@divTrunc(buffer.width, 2)),
.y = @floatFromInt(@divTrunc(buffer.height, 2)),
};
if (!memory.initialized) {
state.camera.offset = mid;
state.grabbed_brick_index = null;
const font_image = loadPng("assets/font.png");
if (font_image) |img| {
state.font_image = img;
}
const indices = [4][4]?BoardIndex{
.{ null, null, null, null },
.{ null, null, null, null },
.{ null, null, null, null },
.{ null, null, null, null },
};
state.bricks[0] = Brick{
.rotating = false,
.rotating_time = 0,
.pos = V2{ .x = 0, .y = 50 },
.color = 0xFF586F52,
.board_indices = indices,
.tiles = [4][4]u32{
.{ 1, 1, 1, 1 },
.{ 1, 0, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
},
};
state.bricks[1] = Brick{
.rotating = false,
.rotating_time = 0,
.pos = V2{ .x = 256, .y = 50 },
.color = 0xFF8299B2,
.board_indices = indices,
.tiles = [4][4]u32{
.{ 2, 2, 2, 0 },
.{ 2, 2, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
},
};
state.bricks[2] = Brick{
.rotating = false,
.rotating_time = 0,
.pos = V2{ .x = 384, .y = 50 },
.color = 0xFF602925,
.board_indices = indices,
.tiles = [4][4]u32{
.{ 3, 3, 3, 0 },
.{ 0, 0, 3, 3 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
},
};
state.bricks[3] = Brick{
.rotating = false,
.rotating_time = 0,
.pos = V2{ .x = 512, .y = 50 },
.color = 0xFFDA983C,
.board_indices = indices,
.tiles = [4][4]u32{
.{ 4, 0, 0, 0 },
.{ 4, 4, 4, 0 },
.{ 0, 0, 4, 0 },
.{ 0, 0, 0, 0 },
},
};
state.bricks[4] = Brick{
.rotating = false,
.rotating_time = 0,
.pos = V2{ .x = 384, .y = 150 },
.color = 0xFF3A5E7A,
.board_indices = indices,
.tiles = [4][4]u32{
.{ 5, 5, 5, 5 },
.{ 0, 5, 0, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
},
};
state.bricks[5] = Brick{
.rotating = false,
.rotating_time = 0,
.pos = V2{ .x = 160, .y = 250 },
.color = 0xFFD69EB0,
.board_indices = indices,
.tiles = [4][4]u32{
.{ 6, 6, 6, 0 },
.{ 6, 0, 0, 0 },
.{ 6, 0, 0, 0 },
.{ 0, 0, 0, 0 },
},
};
state.bricks[6] = Brick{
.rotating = false,
.rotating_time = 0,
.pos = V2{ .x = 192, .y = 350 },
.color = 0xFF9AAB89,
.board_indices = indices,
.tiles = [4][4]u32{
.{ 7, 7, 7, 0 },
.{ 7, 7, 7, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
},
};
state.bricks[7] = Brick{
.rotating = false,
.rotating_time = 0,
.pos = V2{ .x = 224, .y = 450 },
.color = 0xFFBA4816,
.board_indices = indices,
.tiles = [4][4]u32{
.{ 8, 8, 8, 0 },
.{ 8, 0, 8, 0 },
.{ 0, 0, 0, 0 },
.{ 0, 0, 0, 0 },
},
};
const tile_names = [7][7]?[]const u8{
.{ "jan", "feb", "mar", "apr", "may", "jun", null },
.{ "jul", "aug", "sep", "oct", "nov", "dec", null },
.{ "1", "2", "3", "4", "5", "6", "7" },
.{ "8", "9", "10", "11", "12", "13", "14" },
.{ "15", "16", "17", "18", "19", "20", "21" },
.{ "22", "23", "24", "25", "26", "27", "28" },
.{ "29", "30", "31", null, null, null, null },
};
var tiles: [7][7]BoardTile = undefined;
for (tile_names, 0..) |row, i| {
for (row, 0..) |opt, j| {
if (opt) |tile| {
tiles[i][j] = BoardTile.init(tile, false);
} else {
tiles[i][j] = BoardTile.init("", true);
}
}
}
state.board = .{
.pos = mid,
.width = 7,
.height = 7,
.tile_size = V2.init(TILE_SIZE, TILE_SIZE),
.tiles = tiles,
};
const board_size = V2.initI32(
@intCast(state.board.width * TILE_SIZE),
@intCast(state.board.height * TILE_SIZE),
);
const half = board_size.mul(0.5);
state.board.pos = state.board.pos.sub(half);
memory.initialized = true;
}
// Update
const mouse_pos = V2{ .x = @floatCast(input.mouse_x), .y = @floatCast(input.mouse_y) };
const mouse_world_pos = mouse_pos.sub(state.camera.pos);
if (keyPressed(&input.key.f1)) {
state.draw_bounding_rects = !state.draw_bounding_rects;
}
if (!input.mouse_left_down) {
state.grabbed_brick_index = null;
}
for (&state.board.tiles) |*board_row| {
for (board_row) |*board_tile| {
board_tile.hovering = false;
}
}
for (&state.bricks, 0..) |*brick, brick_index| {
if (brick.rotating) {
brick.rotating_time += input.delta_time;
if (brick.rotating_time > 0.05) {
brick.rotating = false;
brick.rotating_time = 0.0;
}
}
for (brick.tiles, 0..) |row, i| {
for (row, 0..) |tile, j| {
if (tile > 0) {
const pos = getTilePos(i, j).add(brick.pos);
const tile_mid = pos.add(tile_size.mul(0.5));
const tile_board_index = &brick.board_indices[i][j];
tile_board_index.* = null;
if (input.mouse_left_down) {
if (pointInsideRect(mouse_world_pos, pos, tile_size)) {
if (state.grabbed_brick_index == null) {
state.grabbed_brick_index = brick_index;
}
}
}
for (&state.board.tiles, 0..) |*board_row, k| {
for (board_row, 0..) |*board_tile, l| {
const board_tile_pos = state.board.getTilePos(k, l);
const in_tile = pointInsideRect(tile_mid, board_tile_pos, state.board.tile_size);
if (in_tile and !board_tile.blocked and !board_tile.taken) {
tile_board_index.* = BoardIndex.init(k, l);
// board_tile.taken = true;
}
if (!board_tile.hovering) {
board_tile.hovering = in_tile;
}
}
}
}
}
}
if (brick_index == state.grabbed_brick_index) {
const diff = mouse_pos.sub(state.mouse_pos);
brick.pos = brick.pos.add(diff);
if (keyPressed(&input.key.e) or keyPressed(&input.key.f)) {
brick.rotating = true;
const rotated = brick.rotate();
brick.tiles = rotated;
const bbox = findBrickBoundingBox(brick);
const center = brick.pos.sub(bbox.center);
brick.pos = mouse_world_pos.add(center);
}
// Flip
if (keyPressed(&input.key.w)) {
brick.rotating = true;
const flipped = brick.transpose();
brick.tiles = flipped;
const bbox = findBrickBoundingBox(brick);
const center = brick.pos.sub(bbox.center);
brick.pos = mouse_world_pos.add(center);
}
}
if (!input.mouse_left_down) {
const valid_pos = brickHasValidPos(brick);
if (valid_pos) {
// TODO: Snap to board
const min = findBrickMinBoardIdx(brick);
const board_tile_pos = state.board.getTilePos(min.i, min.j);
const bbox = findBrickBoundingBox(brick);
const diff = brick.pos.sub(bbox.min);
brick.pos = board_tile_pos.add(diff);
}
}
}
if (input.mouse_left_down and state.grabbed_brick_index == null) {
const diff = mouse_pos.sub(state.mouse_pos);
state.camera.pos = state.camera.pos.add(diff);
}
state.mouse_pos = mouse_pos;
// Render
fillRect(buffer, 0, 0, buffer.width, buffer.height, 0xFFFFFFFF);
// Draw board
{
const board = state.board;
for (board.tiles, 0..) |row, i| {
for (row, 0..) |tile, j| {
const tile_pos = board.getTilePos(i, j);
const width: i32 = @intFromFloat(board.tile_size.x);
const height: i32 = @intFromFloat(board.tile_size.y);
const str_width = @divFloor(TILE_SIZE * tile.label.len, 4);
const str_height = @divFloor(32, 4);
const str_size = V2.initUSize(str_width, str_height);
const string_pos = tile_pos.add(board.tile_size.mul(0.5)).sub(str_size.mul(0.5));
var color: u32 = 0xFF000000;
if (tile.blocked) {
color = 0xFFFFFFFF;
} else if (tile.hovering) {
color = 0xFF00FF00;
}
const camera_pos = tile_pos.add(state.camera.pos);
const pos_x: i32 = @intFromFloat(camera_pos.x);
const pos_y: i32 = @intFromFloat(camera_pos.y);
fillRect(buffer, pos_x, pos_y, width, height, color);
drawRect(buffer, camera_pos, board.tile_size, 0xFFFFFFFF, 2);
const string_camera_pos = string_pos.add(state.camera.pos);
drawString(buffer, state.font_image, string_camera_pos, tile.label);
}
}
}
for (state.bricks) |brick| {
const bounding_box = findBrickBoundingBox(&brick);
for (brick.tiles, 0..) |row, i| {
for (row, 0..) |tile, j| {
if (tile > 0) {
const pos = getTilePos(i, j).add(brick.pos);
const camera_pos = pos.add(state.camera.pos);
var color: u32 = if (brick.rotating) 0xFF007800 else brick.color;
if (comptime builtin.mode == std.builtin.OptimizeMode.Debug) {
const board_index = brick.board_indices[i][j];
if (board_index != null) {
color = 0xFFFF0000;
}
}
fillSquare(
buffer,
@intFromFloat(camera_pos.x),
@intFromFloat(camera_pos.y),
TILE_SIZE,
color,
);
}
}
}
if (comptime builtin.mode == std.builtin.OptimizeMode.Debug) {
if (state.draw_bounding_rects) {
{
const camera_pos = brick.pos.add(state.camera.pos);
drawRect(buffer, camera_pos, .{ .x = 4 * TILE_SIZE, .y = 4 * TILE_SIZE }, 0xFFFF0000, 1);
}
{
const bbox_size = bounding_box.max.sub(bounding_box.min);
const camera_pos = bounding_box.min.add(state.camera.pos);
drawRect(buffer, camera_pos, bbox_size, 0xFFFF0000, 1);
}
{
const size = 8;
const half = @divTrunc(size, 2);
const camera_pos = bounding_box.center.add(state.camera.pos);
const x: i32 = @intFromFloat(camera_pos.x);
const y: i32 = @intFromFloat(camera_pos.y);
fillSquare(buffer, x - half, y - half, 8, 0xFFFF0000);
}
}
}
}
state.frame_count += 1;
}