547 lines
18 KiB
Zig
547 lines
18 KiB
Zig
const std = @import("std");
|
|
const sdl = @import("sdl");
|
|
const math = @import("math.zig");
|
|
const err = @import("error.zig");
|
|
const Game = @import("game.zig");
|
|
const Graphics = @import("graphics.zig");
|
|
const Assets = @import("assets.zig");
|
|
|
|
const Id = u32;
|
|
const Order = i32;
|
|
|
|
pub var object_map: std.AutoHashMapUnmanaged(Id, usize) = .{};
|
|
pub var objects: std.ArrayListUnmanaged(Object) = .{};
|
|
|
|
pub var hand: Assets.Object = undefined;
|
|
pub var table: Assets.Object = undefined;
|
|
pub var cubemap: Assets.Object = undefined;
|
|
|
|
pub var camera_position: @Vector(2, f32) = @splat(0);
|
|
pub var hand_transform: Graphics.Transform = .{};
|
|
pub var dock_transform: Graphics.Transform = .{};
|
|
pub var zoom: i32 = 0;
|
|
|
|
pub var hover: ?Id = null;
|
|
pub var panning = false;
|
|
pub var hand_objects: u32 = 0;
|
|
pub var hand_scale: f32 = 0;
|
|
pub var dock_objects: u32 = 0;
|
|
pub var dock_last_width: f32 = 0;
|
|
pub var dock_focused: bool = false;
|
|
pub var dock_spacing: f32 = 0;
|
|
pub var min_order: Order = undefined;
|
|
pub var max_order: Order = undefined;
|
|
|
|
const DOCK_TILT = 0.03;
|
|
const DOCK_TILT_SIN = std.math.sin(DOCK_TILT);
|
|
const DOCK_TILT_COS = std.math.cos(DOCK_TILT);
|
|
|
|
const Object = struct {
|
|
type: Type,
|
|
transform: Graphics.Transform = .{},
|
|
target_transform: Graphics.Transform = .{},
|
|
width: f32,
|
|
height: f32,
|
|
object: Assets.Object,
|
|
order: Order,
|
|
id: Id,
|
|
index: u32,
|
|
z: u32 = 0,
|
|
parent: Parent = .none,
|
|
parent_index: u32 = 0,
|
|
child_last_id: u32 = 0,
|
|
influence: f32 = 0,
|
|
|
|
const Type = enum {
|
|
card,
|
|
deck,
|
|
};
|
|
const Parent = union(enum) {
|
|
none,
|
|
hand,
|
|
dock,
|
|
deck: Id,
|
|
};
|
|
|
|
pub fn reparent(self: *@This(), new_parent: Parent) void {
|
|
self.transform = self.drawingTransform();
|
|
self.influence = 0;
|
|
self.parent = new_parent;
|
|
}
|
|
pub fn drawingTransform(self: @This()) Graphics.Transform {
|
|
const transform = self.transform;
|
|
const parent_transform = switch (self.parent) {
|
|
.hand => World.hand_transform,
|
|
.dock => World.dock_transform,
|
|
.deck => |deck| if (World.getObject(deck)) |object| object.drawingTransform() else Graphics.Transform{},
|
|
.none => return transform,
|
|
};
|
|
return Graphics.Transform.combineTransforms(
|
|
transform,
|
|
Graphics.Transform.lerpTransform(
|
|
.{},
|
|
parent_transform,
|
|
self.influence,
|
|
),
|
|
);
|
|
}
|
|
};
|
|
|
|
const World = @This();
|
|
pub fn initDebug() void {
|
|
for (0..70) |i| {
|
|
World.objects.append(Game.alloc, .{
|
|
.type = .card,
|
|
.width = 0.5,
|
|
.height = 0.5,
|
|
.object = Assets.load(.gltf, "data/yakuza/card.gltf"),
|
|
.order = @intCast(i),
|
|
.id = @intCast(i),
|
|
.index = @intCast(i),
|
|
.parent = .{ .deck = if (i < 60) @as(Id, 70) else @as(Id, 71) },
|
|
}) catch err.oom();
|
|
World.object_map.put(Game.alloc, @intCast(i), i) catch err.oom();
|
|
}
|
|
World.objects.append(Game.alloc, .{
|
|
.target_transform = .{ .position = .{ -3, 0, 0 } },
|
|
.type = .deck,
|
|
.width = 1,
|
|
.height = 1,
|
|
.object = Assets.load(.gltf, "data/yakuza/pad.gltf"),
|
|
.order = 70,
|
|
.id = 70,
|
|
.index = 70,
|
|
}) catch err.oom();
|
|
World.object_map.put(Game.alloc, 60, 60) catch err.oom();
|
|
World.objects.append(Game.alloc, .{
|
|
.target_transform = .{ .position = .{ 3, 0, 0 } },
|
|
.type = .deck,
|
|
.width = 1,
|
|
.height = 1,
|
|
.object = Assets.load(.gltf, "data/yakuza/pad.gltf"),
|
|
.order = 71,
|
|
.id = 71,
|
|
.index = 71,
|
|
}) catch err.oom();
|
|
World.object_map.put(Game.alloc, 71, 71) catch err.oom();
|
|
|
|
World.hand = Assets.load(.gltf, "data/hand.gltf");
|
|
World.table = Assets.load(.gltf, "data/yakuza/table.gltf");
|
|
World.cubemap = Assets.load(.gltf, "data/cubemap.gltf");
|
|
|
|
World.camera_position = @splat(0);
|
|
World.hand_transform = .{};
|
|
World.hand_scale = 0.5;
|
|
World.dock_transform = .{
|
|
.position = .{ 0, 0, 4 },
|
|
};
|
|
World.dock_spacing = 0.2;
|
|
World.zoom = 0;
|
|
|
|
World.panning = false;
|
|
World.dock_focused = false;
|
|
World.min_order = 0;
|
|
World.max_order = 71;
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
Assets.free(World.hand);
|
|
Assets.free(World.table);
|
|
Assets.free(World.cubemap);
|
|
for (World.objects.items) |*object| {
|
|
Assets.free(object.object);
|
|
}
|
|
World.objects.clearAndFree(Game.alloc);
|
|
World.object_map.clearAndFree(Game.alloc);
|
|
}
|
|
|
|
pub fn update(delta: f32) void {
|
|
World.updateCamera(delta);
|
|
{
|
|
World.dock_transform = Graphics.Transform.lerpTransformTimeLn(
|
|
World.dock_transform,
|
|
Graphics.Transform.combineTransforms(.{ .position = .{
|
|
0,
|
|
-1,
|
|
-1 / Graphics.camera.lens,
|
|
} }, Graphics.camera.transform),
|
|
delta,
|
|
-128,
|
|
);
|
|
}
|
|
{
|
|
const hand_target = Graphics.camera.raycast(.{ Game.mouse.x_norm, Game.mouse.y_norm }, .{ 0, 0, 1, 0 });
|
|
World.hand_transform.position = math.lerpTimeLn(
|
|
World.hand_transform.position,
|
|
hand_target + @Vector(3, f32){ 0, 0, 0.2 },
|
|
delta,
|
|
-24,
|
|
);
|
|
}
|
|
|
|
World.updateOrder();
|
|
|
|
World.hover = null;
|
|
World.hand_objects = 0;
|
|
World.dock_objects = 0;
|
|
for (World.objects.items) |*object| {
|
|
updateHover(object);
|
|
}
|
|
for (World.objects.items) |*object| {
|
|
updateObject(object, delta);
|
|
}
|
|
World.updateControls();
|
|
}
|
|
|
|
pub fn updateControls() void {
|
|
if (Game.keyboard.keys.is_pressed(sdl.SDL_SCANCODE_LSHIFT)) {
|
|
World.scroll(Game.mouse.wheel);
|
|
} else {
|
|
World.zoom = std.math.clamp(World.zoom + Game.mouse.wheel, -4, 8);
|
|
}
|
|
if (Game.mouse.buttons.is_just_pressed(sdl.BUTTON_LEFT)) {
|
|
World.panning = !World.tryPick();
|
|
}
|
|
if (Game.mouse.buttons.is_just_pressed(sdl.BUTTON_RIGHT)) {
|
|
_ = World.tryRelease();
|
|
}
|
|
if (Game.mouse.y_norm <= -0.8) {
|
|
World.dock_focused = true;
|
|
}
|
|
if (Game.mouse.y_norm >= -0.6) {
|
|
World.dock_focused = false;
|
|
}
|
|
}
|
|
|
|
pub fn scroll(delta: i32) void {
|
|
if (World.getHover()) |hover_object| {
|
|
if (hover_object.type == .deck) {
|
|
if (delta > 0) {
|
|
var left_to_put = delta;
|
|
var i = World.objects.items.len - 1;
|
|
while (left_to_put > 0) {
|
|
const object = &World.objects.items[i];
|
|
if (object.parent != .hand) {
|
|
if (i == 0) break;
|
|
i -= 1;
|
|
continue;
|
|
}
|
|
|
|
World.bringToTop(object);
|
|
object.reparent(.{ .deck = hover_object.id });
|
|
if (i == 0) break;
|
|
i -= 1;
|
|
left_to_put -= 1;
|
|
}
|
|
}
|
|
if (delta < 0) {
|
|
var left_to_take = -delta;
|
|
var i = World.objects.items.len - 1;
|
|
while (left_to_take > 0) {
|
|
const object = &World.objects.items[i];
|
|
if (object.parent != .deck or object.parent.deck != hover_object.id) {
|
|
if (i == 0) break;
|
|
i -= 1;
|
|
continue;
|
|
}
|
|
|
|
World.bringToTop(object);
|
|
object.reparent(.hand);
|
|
if (i == 0) break;
|
|
i -= 1;
|
|
left_to_take -= 1;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (delta > 0 and World.hand_objects > 0) {
|
|
var left_to_scroll = @rem(delta, @as(i32, @intCast(World.hand_objects)));
|
|
var i = World.objects.items.len - 1;
|
|
while (left_to_scroll > 0) : (i -= 1) {
|
|
const object = &World.objects.items[i];
|
|
if (object.parent != .hand) continue;
|
|
|
|
World.bringToBottom(object);
|
|
left_to_scroll -= 1;
|
|
}
|
|
}
|
|
if (delta < 0 and World.hand_objects > 0) {
|
|
var left_to_scroll = @rem(-delta, @as(i32, @intCast(World.hand_objects)));
|
|
var i: usize = 0;
|
|
while (left_to_scroll > 0) : (i += 1) {
|
|
const object = &World.objects.items[i];
|
|
if (object.parent != .hand) continue;
|
|
|
|
World.bringToTop(object);
|
|
left_to_scroll -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn tryPick() bool {
|
|
var object = World.getHover() orelse return false;
|
|
switch (object.type) {
|
|
.card => {},
|
|
.deck => {
|
|
if (!Game.keyboard.keys.is_pressed(sdl.SDL_SCANCODE_LSHIFT)) {
|
|
for (World.objects.items) |*child| {
|
|
if (child.parent == .deck and child.parent.deck == object.id and child.id == object.child_last_id) {
|
|
object = child;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
World.panning = false;
|
|
object.reparent(.hand);
|
|
World.bringToTop(object);
|
|
return true;
|
|
}
|
|
pub fn tryRelease() bool {
|
|
const object = blk: {
|
|
var i = World.objects.items.len - 1;
|
|
while (true) {
|
|
const object = &World.objects.items[i];
|
|
if (object.parent == .hand) {
|
|
break :blk object;
|
|
}
|
|
if (i > 0)
|
|
i -= 1
|
|
else
|
|
return false;
|
|
}
|
|
};
|
|
object.target_transform.position = World.hand_transform.position;
|
|
World.bringToTop(object);
|
|
if (object.type == .card and !Game.keyboard.keys.is_pressed(sdl.SDL_SCANCODE_LSHIFT)) {
|
|
if (World.getHover()) |hover_object| {
|
|
if (hover_object.type == .deck) {
|
|
object.reparent(.{ .deck = hover_object.id });
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
if (World.dock_focused) {
|
|
object.reparent(.dock);
|
|
return true;
|
|
} else {
|
|
object.reparent(.none);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
pub fn updateHover(object: *Object) void {
|
|
switch (object.parent) {
|
|
.deck => |id| {
|
|
if (World.getObject(id)) |deck| {
|
|
deck.child_last_id = object.id;
|
|
}
|
|
},
|
|
.none => {
|
|
if (!World.dock_focused and Graphics.camera.mouse_in_quad(.{ Game.mouse.x_norm, Game.mouse.y_norm }, object.drawingTransform(), object.width, object.height)) {
|
|
if (World.hover == null or World.getHover().?.z < object.z) {
|
|
World.hover = object.id;
|
|
}
|
|
}
|
|
},
|
|
.hand => {
|
|
object.parent_index = World.hand_objects;
|
|
World.hand_objects += 1;
|
|
},
|
|
.dock => {
|
|
object.parent_index = World.dock_objects;
|
|
World.dock_last_width = object.width * object.target_transform.scale;
|
|
World.dock_objects += 1;
|
|
if (World.dock_focused and Graphics.camera.mouse_in_quad(.{ Game.mouse.x_norm, Game.mouse.y_norm }, object.transform.combineTransforms(World.dock_transform), object.width, object.height)) {
|
|
if (World.hover == null or World.getObject(World.hover.?).?.z < object.z) {
|
|
World.hover = object.id;
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn updateObject(object: *Object, delta: f32) void {
|
|
switch (object.parent) {
|
|
.none => {
|
|
object.target_transform.position[2] = @as(f32, 0.001) * @as(f32, @floatFromInt(object.index + 1));
|
|
object.target_transform.scale = if (World.hover == object.id) @as(f32, 1.1) else @as(f32, 1);
|
|
},
|
|
.hand => {
|
|
var target_position = @as(@Vector(3, f32), @splat(0));
|
|
var target_scale: f32 = 1.0;
|
|
target_position[2] -= 0.001;
|
|
const hand_order = hand_objects - object.parent_index - 1;
|
|
switch (hand_order) {
|
|
0 => {},
|
|
else => |i| {
|
|
target_position[0] += World.hand_scale * if (i & 2 == 0) @as(f32, 1) else @as(f32, 1.5);
|
|
target_position[1] += World.hand_scale * if ((i - 1) & 2 == 0) @as(f32, -0.25) else @as(f32, -0.75);
|
|
target_position[2] -= @as(f32, @floatFromInt((hand_order - 1) / 4)) * 0.001;
|
|
target_scale = 0.5;
|
|
},
|
|
}
|
|
object.target_transform.position = target_position;
|
|
object.target_transform.scale = target_scale;
|
|
},
|
|
.dock => {
|
|
var topleft_x = -World.dock_last_width * 0.5 * DOCK_TILT_COS + World.dock_spacing * (@as(f32, @floatFromInt(object.parent_index)) - @as(f32, @floatFromInt(World.dock_objects - 1)) * 0.5);
|
|
const total_w = @as(f32, @floatFromInt(World.dock_objects - 1)) * World.dock_spacing + World.dock_last_width * DOCK_TILT_COS;
|
|
const mouse_x = if (World.dock_focused) Game.mouse.x_norm else 0.5;
|
|
if (total_w > Graphics.camera.aspect * 2) {
|
|
topleft_x += math.lerp(0, Graphics.camera.aspect - total_w * 0.5, mouse_x);
|
|
}
|
|
const hit = World.hover == object.id;
|
|
const topleft_y = if (World.dock_focused) if (hit) @as(f32, 0.5) else @as(f32, 0.3) else @as(f32, 0.2);
|
|
object.target_transform.position = .{
|
|
topleft_x + object.width * 0.5 * object.target_transform.scale * DOCK_TILT_COS,
|
|
topleft_y - object.height * 0.5 * object.target_transform.scale,
|
|
if (hit) @as(f32, 0.02) else -object.width * 0.5 * DOCK_TILT_SIN,
|
|
};
|
|
object.target_transform.rotation = if (hit)
|
|
Graphics.Transform.ZERO.rotation
|
|
else
|
|
Graphics.Transform.rotationByAxis(.{ 0, 1, 0 }, DOCK_TILT);
|
|
},
|
|
.deck => {
|
|
object.target_transform.position = .{ 0, 0, @as(f32, 0.001) };
|
|
object.target_transform.scale = if (World.hover == object.id) @as(f32, 1.1) else @as(f32, 1);
|
|
},
|
|
}
|
|
object.z = switch (object.parent) {
|
|
.deck => |deck_id| if (World.getObject(deck_id)) |deck| deck.z + object.index else object.index,
|
|
.none, .hand, .dock => object.index,
|
|
};
|
|
if (object.parent != .none) {
|
|
object.influence = math.lerpTimeLn(
|
|
object.influence,
|
|
1.0,
|
|
delta,
|
|
-24,
|
|
);
|
|
}
|
|
object.transform = Graphics.Transform.lerpTransformTimeLn(
|
|
object.transform,
|
|
object.target_transform,
|
|
delta,
|
|
-24,
|
|
);
|
|
}
|
|
|
|
pub fn draw() void {
|
|
Graphics.drawObject(&World.table, .{});
|
|
|
|
for (World.objects.items) |*object| {
|
|
sw: switch (object.parent) {
|
|
.none, .hand => {
|
|
Graphics.drawObject(&object.object, object.drawingTransform());
|
|
},
|
|
.dock => {},
|
|
.deck => |id| {
|
|
if (World.getObject(id)) |deck| {
|
|
if (deck.child_last_id != object.id) continue;
|
|
}
|
|
continue :sw .none;
|
|
},
|
|
}
|
|
}
|
|
|
|
Graphics.drawObject(
|
|
&World.hand,
|
|
Graphics.Transform.combineTransforms(
|
|
.{
|
|
.position = .{ World.hand_scale * 0.5, -World.hand_scale * 0.5, 0 },
|
|
.scale = World.hand_scale,
|
|
},
|
|
World.hand_transform,
|
|
),
|
|
);
|
|
|
|
Graphics.drawObject(&World.cubemap, .{
|
|
.scale = 1e20,
|
|
.position = Graphics.camera.transform.position,
|
|
});
|
|
Graphics.clearDepth();
|
|
for (World.objects.items) |*object| {
|
|
if (object.parent == .dock)
|
|
Graphics.drawObject(&object.object, object.drawingTransform());
|
|
}
|
|
}
|
|
|
|
pub fn updateCamera(delta: f32) void {
|
|
const zoom_factor = std.math.exp(@as(f32, @floatFromInt(zoom)) * @log(2.0) * -0.5);
|
|
|
|
if (Game.mouse.buttons.is_pressed(sdl.BUTTON_LEFT)) {
|
|
if (World.panning) {
|
|
World.camera_position[0] += zoom_factor * Game.mouse.dx / @as(f32, @floatFromInt(Graphics.window_width)) * -15;
|
|
World.camera_position[1] += zoom_factor * Game.mouse.dy / @as(f32, @floatFromInt(Graphics.window_height)) * 15;
|
|
}
|
|
}
|
|
|
|
const offset = @Vector(3, f32){ 0.0, -1.0 * zoom_factor, 4.0 * zoom_factor };
|
|
const target_position = @Vector(3, f32){ World.camera_position[0], World.camera_position[1], 0.0 };
|
|
Graphics.camera.transform.position = math.lerpTimeLn(
|
|
Graphics.camera.transform.position,
|
|
target_position + offset,
|
|
delta,
|
|
-32,
|
|
);
|
|
|
|
const ORIGIN_DIR = @Vector(3, f32){ 0.0, 0.0, -1.0 };
|
|
const INIT_ROTATION = Graphics.Transform.rotationByAxis(.{ 1.0, 0.0, 0.0 }, std.math.pi * 0.5);
|
|
|
|
const ROTATED_DIR = Graphics.Transform.rotateVector(ORIGIN_DIR, INIT_ROTATION);
|
|
|
|
const target_rotation = Graphics.Transform.combineRotations(
|
|
INIT_ROTATION,
|
|
Graphics.Transform.rotationToward(
|
|
ROTATED_DIR,
|
|
math.lerp(-offset, target_position - Graphics.camera.transform.position, 0.125),
|
|
.{ .normalize_to = true },
|
|
),
|
|
);
|
|
Graphics.camera.transform.rotation = Graphics.Transform.normalizeRotation(math.slerpTimeLn(
|
|
Graphics.camera.transform.rotation,
|
|
target_rotation,
|
|
delta,
|
|
-16,
|
|
));
|
|
}
|
|
|
|
fn getHover() ?*Object {
|
|
if (World.hover) |id| {
|
|
return World.getObject(id);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn getObject(id: Id) ?*Object {
|
|
const index = World.object_map.get(id) orelse return null;
|
|
if (index >= World.objects.items.len) return null;
|
|
return &World.objects.items[index];
|
|
}
|
|
|
|
fn bringToTop(object: *Object) void {
|
|
World.max_order += 1;
|
|
object.order = World.max_order;
|
|
}
|
|
fn bringToBottom(object: *Object) void {
|
|
World.min_order -= 1;
|
|
object.order = World.min_order;
|
|
}
|
|
|
|
fn updateOrder() void {
|
|
std.sort.block(Object, World.objects.items, {}, objectOrderLessThan);
|
|
World.object_map.clearRetainingCapacity();
|
|
for (0.., World.objects.items) |i, *object| {
|
|
object.index = @intCast(i);
|
|
World.object_map.putAssumeCapacityNoClobber(object.id, i);
|
|
}
|
|
}
|
|
|
|
fn objectOrderLessThan(ctx: void, lhs: Object, rhs: Object) bool {
|
|
_ = ctx;
|
|
return lhs.order < rhs.order;
|
|
}
|