kble/src/main.zig

256 lines
9.2 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.
const ray = @cImport({
@cInclude("raylib.h");
});
const std = @import("std");
const maxInt = std.math.maxInt;
const log = std.log.info;
const conf = @import("conf.zig");
const Level = @import("level.zig");
const Vec2 = @import("vec2.zig");
const Rect = @import("rect.zig");
const Mouse = @import("mouse.zig");
const draw = @import("draw.zig");
const movement = @import("movement.zig");
const verbs = @import("verbs.zig");
const scaling = @import("scaling.zig");
const actions = @import("actions.zig");
const ActionsDef = actions.ActionsDef;
const ActionCat = actions.ActionCat;
const Mode = @import("modes.zig").Mode;
const char_range = 255;
pub fn main() void {
const level_path: [*:0]const u8 = if (std.os.argv.len > 1)
std.os.argv[1]
else nopath: {
std.log.notice("No path provided, defaults to \"level.kble\".", .{});
break :nopath "level.kble";
};
// Create allocator
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = &arena.allocator;
// Create window.
ray.SetConfigFlags(ray.FLAG_WINDOW_RESIZABLE);
ray.InitWindow(640, 480, "KBLE");
defer ray.CloseWindow();
// Limit FPS for performance.
ray.SetTargetFPS(60);
// Try to load level, is doesn't exist create it.
var level: Level = Level.init_read(allocator, level_path) catch create: {
break :create Level.init(allocator, 16, 16) catch unreachable;
};
defer level.deinit(allocator);
// Create camera.
// TODO: Can't move camera, implement camera mode.
var camera: Vec2 = comptime Vec2.init(0, 0);
// Init scale, used by drawing code.
var scale: scaling.scale_type = scaling.scale_default;
// Create mouse.
var mouse: Mouse = comptime Mouse.init();
// Create cursor.
var cursor: Vec2 = comptime Vec2.init(0, 0);
// Used by rectangle selection mode.
var cursor_before: Vec2 = comptime Vec2.init(0, 0);
// Set default mode.
var mode: Mode = Mode.normal;
// Create binding "table".
var bindings = [_]*const actions.Action{&actions.ActionsDef.none} ** char_range;
// Set default bindings.
// Movement.
bindings['h'] = &ActionsDef.move_left;
bindings['j'] = &ActionsDef.move_down;
bindings['k'] = &ActionsDef.move_up;
bindings['l'] = &ActionsDef.move_right;
bindings['y'] = &ActionsDef.move_up_left;
bindings['u'] = &ActionsDef.move_up_right;
bindings['b'] = &ActionsDef.move_down_left;
bindings['n'] = &ActionsDef.move_down_right;
// Verbs.
bindings[' '] = &ActionsDef.verb_clear_selection;
bindings['d'] = &ActionsDef.verb_delete;
bindings['r'] = &ActionsDef.verb_replace;
// Scale.
bindings['='] = &ActionsDef.scale_reset;
bindings['+'] = &ActionsDef.scale_up;
bindings['-'] = &ActionsDef.scale_down;
// File.
bindings['e'] = &ActionsDef.file_read;
bindings['w'] = &ActionsDef.file_write;
// Mode.
bindings['\n'] = &ActionsDef.mode_normal;
bindings['i'] = &ActionsDef.mode_select;
bindings['v'] = &ActionsDef.mode_rectangle;
bindings['c'] = &ActionsDef.mode_camera;
// Create input buffer.
const input_buffer_len = 255;
var input_buffer: [input_buffer_len]u32 = undefined;
comptime {
var i: u8 = 0;
while (i < input_buffer_len) : (i += 1) {
input_buffer[i] = 0;
}
}
var input_cursor: u8 = 0;
while (!ray.WindowShouldClose()) {
{
// TODO: apply DRY
// Get keyboard input.
var key = ray.GetCharPressed();
// Check if more characters have been pressed.
while (key != 0) {
// Add key to buffer.
input_buffer[input_cursor] = @intCast(u32, key);
input_cursor += 1;
// Avoid writing out of memory.
if (input_cursor >= input_buffer_len)
input_cursor = input_buffer_len - 1;
key = ray.GetCharPressed();
}
// Check for special keys, not detected by GetCharPressed.
if (ray.IsKeyDown(ray.KEY_ENTER)) {
input_buffer[input_cursor] = '\n';
input_cursor += 1;
// Avoid writing out of memory.
if (input_cursor >= input_buffer_len)
input_cursor = input_buffer_len - 1;
}
}
// Process buffer content.
// Read the buffer backwards. This is placeholder logic.
while (input_cursor > 0) {
input_cursor -= 1;
const key = if (input_buffer[input_cursor] <= char_range)
input_buffer[input_cursor]
else
0;
// TODO: move everything from this to a function (for config and macros support).
const action: actions.Action = bindings[key].*;
const apply_selection: bool = (mode == Mode.select);
switch (action.category) {
.none => std.log.info("No action bound to {}.", .{key}),
.movement => {
const selection_update: movement.SelectionUpdate =
action.function_move(&cursor, 1);
if (apply_selection and selection_update.active) {
level.apply_selection_update(selection_update);
}
},
.verb => {
action.function_verb(&level, 1);
},
.scale => {
scale = action.function_scale(scale);
},
.file => {
action.function_file(&level, allocator, level_path);
},
.mode => {
// Rectangle selection!
if (mode == Mode.rectangle) {
const selection = Rect.init_from_vec2(cursor, cursor_before);
level.select_rect(selection, true);
}
cursor_before = cursor; // Save position before selection.
mode = action.next_mode;
if (mode == Mode.select) // Select first tile.
level.select_cell(cursor, true);
},
}
// TODO end
}
// Mouse operations.
if (conf.mouse_enabled) {
// Update position.
{
// Set mouse scaling.
ray.SetMouseScale(1.0 / @intToFloat(f32, scale), 1.0 / @intToFloat(f32, scale));
mouse.pos.x = @intCast(Vec2.int_type, ray.GetMouseX()) + camera.x;
mouse.pos.y = @intCast(Vec2.int_type, ray.GetMouseY()) + camera.y;
}
const left_click: bool = ray.IsMouseButtonPressed(conf.mouse_left_btn);
const right_click: bool = ray.IsMouseButtonPressed(conf.mouse_right_btn);
const click: bool = left_click or right_click;
// If click, get out of current mode and apply changes if necessary.
if (click and mouse.mode != Mouse.MouseMode.idle) {
defer mouse.mode = Mouse.MouseMode.wait;
// Select area.
if (mouse.mode == Mouse.MouseMode.rect_sel) {
const selection = Rect.init_from_vec2(mouse.start_pos, mouse.pos);
level.select_rect(selection, true);
}
}
// State machine.
switch (mouse.mode) {
.wait => mouse.mode = Mouse.MouseMode.idle,
// See if can switch mode.
.idle => {
const mod_rect_sel: bool = ray.IsKeyDown(ray.KEY_LEFT_SHIFT) or
ray.IsKeyDown(ray.KEY_RIGHT_SHIFT);
if (click) { // Switch mode.
cursor = mouse.pos;
// Reset selection on right click.
if (right_click) {
verbs.clear_selection(&level, 1);
}
// Set position.
mouse.start_pos = mouse.pos;
mouse.mode = if (mod_rect_sel)
Mouse.MouseMode.rect_sel
else
Mouse.MouseMode.sel;
}
},
// Select stuff under the cursor.
.sel => {
cursor = mouse.pos;
level.select_cell(mouse.pos, true);
},
else => {},
}
}
ray.BeginDrawing();
defer ray.EndDrawing();
ray.ClearBackground(ray.BLACK);
level.draw(scale, camera);
level.draw_selection(scale, camera);
draw.cursor(scale, camera, cursor, mode);
if (mode == Mode.rectangle)
draw.rectangle_selection(scale, camera, Rect.init_from_vec2(cursor, cursor_before));
mouse.draw(scale, camera);
//ray.DrawFPS(0, 0);
}
}