This commit is contained in:
2026-04-05 20:56:43 +02:00
parent 6e0ecaed83
commit 74262db40f
2 changed files with 33 additions and 167 deletions

View File

@@ -1,19 +1,19 @@
const std = @import("std");
const Type = enum {
pub const Type = enum {
ailment,
recipe,
ingredient,
};
const NodeID = enum {
pub const NodeID = enum {
sellery_juice,
salt_craving,
};
const Node = struct {
name: []const u8,
description: []const u8,
pub const Node = struct {
name: [:0]const u8,
description: [:0]const u8,
type: Type,
};
@@ -23,28 +23,7 @@ const Edge = struct {
dest: NodeID,
};
fn validated(arr: std.EnumArray(NodeID, Node)) std.EnumArray(NodeID, Node) {
for (std.enums.values(NodeID)) |id| {
const desc = arr.get(id).description;
var i: usize = 0;
while (i + 1 < desc.len) : (i += 1) {
if (desc[i] == '[' and desc[i + 1] == '[') {
const start = i + 2;
var j = start;
while (j + 1 < desc.len and !(desc[j] == ']' and desc[j + 1] == ']')) : (j += 1) {}
const ref = desc[start..j];
if (std.meta.stringToEnum(NodeID, ref) == null) {
@compileLog("Unknown node reference: ", ref);
@compileError("Invalid wiki link in node description (see log above)");
}
i = j + 1;
}
}
}
return arr;
}
pub const nodes = validated(std.EnumArray(NodeID, Node).init(.{
pub const nodes = std.EnumArray(NodeID, Node).init(.{
.sellery_juice = .{
.name = "Sellery Juice",
.description = "A healing drink made from celery.",
@@ -52,8 +31,12 @@ pub const nodes = validated(std.EnumArray(NodeID, Node).init(.{
},
.salt_craving = .{
.name = "Salt craving",
.description = "Can be caused by sodium deficincy. Can be fixed with [[sellery_juice]].",
.type = .recipe,
.description = "Can be caused by sodium deficincy. Can be fixed with " ++ linkTo(.sellery_juice) ++ ".",
.type = .ailment,
},
}));
});
var edges: []const Edge = &.{};
fn linkTo(node: NodeID) []const u8 {
return "[[" ++ @tagName(node) ++ "]]";
}

View File

@@ -3,9 +3,6 @@ const rl = @import("raylib");
const builtin = @import("builtin");
const math = std.math;
const data = @import("data.zig");
comptime {
_ = data.nodes;
}
const NODE_R: f32 = 42.0;
@@ -25,125 +22,45 @@ const ORBIT_R: f32 = 220.0;
// ── Data model ───────────────────────────────────────────────────────────────
const NodeKind = enum { body_part, ailment, recipe, ingredient, property };
const NodeDef = struct {
id: u8,
name: [:0]const u8,
kind: NodeKind,
links: []const u8,
};
/// All content is hardcoded and will be embedded in the binary at compile time.
const all_nodes = [_]NodeDef{
// Body systems (04)
.{ .id = 0, .name = "Digestive System", .kind = .body_part, .links = &.{ 10, 11, 12 } },
.{ .id = 1, .name = "Immune System", .kind = .body_part, .links = &.{ 13, 14 } },
.{ .id = 2, .name = "Nervous System", .kind = .body_part, .links = &.{15} },
.{ .id = 3, .name = "Circulatory", .kind = .body_part, .links = &.{ 13, 14 } },
.{ .id = 4, .name = "Endocrine", .kind = .body_part, .links = &.{ 14, 15 } },
// Ailments (1015)
.{ .id = 10, .name = "Poor Digestion", .kind = .ailment, .links = &.{ 20, 21 } },
.{ .id = 11, .name = "Constipation", .kind = .ailment, .links = &.{ 20, 22 } },
.{ .id = 12, .name = "Bloating", .kind = .ailment, .links = &.{21} },
.{ .id = 13, .name = "Inflammation", .kind = .ailment, .links = &.{ 22, 23 } },
.{ .id = 14, .name = "Fatigue", .kind = .ailment, .links = &.{ 21, 23 } },
.{ .id = 15, .name = "Brain Fog", .kind = .ailment, .links = &.{23} },
// Recipes (2023)
.{ .id = 20, .name = "Meat Smoothie", .kind = .recipe, .links = &.{ 30, 31, 32 } },
.{ .id = 21, .name = "Raw Milk Kefir", .kind = .recipe, .links = &.{ 33, 34 } },
.{ .id = 22, .name = "Raw Egg Blend", .kind = .recipe, .links = &.{ 31, 32, 34 } },
.{ .id = 23, .name = "Honey-Butter Mix", .kind = .recipe, .links = &.{ 35, 36 } },
// Ingredients (3036)
.{ .id = 30, .name = "Raw Meat", .kind = .ingredient, .links = &.{ 40, 41 } },
.{ .id = 31, .name = "Raw Eggs", .kind = .ingredient, .links = &.{ 40, 42 } },
.{ .id = 32, .name = "Lemon Juice", .kind = .ingredient, .links = &.{43} },
.{ .id = 33, .name = "Raw Milk", .kind = .ingredient, .links = &.{ 42, 44 } },
.{ .id = 34, .name = "Kefir Grains", .kind = .ingredient, .links = &.{44} },
.{ .id = 35, .name = "Raw Honey", .kind = .ingredient, .links = &.{ 41, 43 } },
.{ .id = 36, .name = "Raw Butter", .kind = .ingredient, .links = &.{ 41, 44 } },
// Properties (4044)
.{ .id = 40, .name = "High Protein", .kind = .property, .links = &.{} },
.{ .id = 41, .name = "Energy Dense", .kind = .property, .links = &.{} },
.{ .id = 42, .name = "Probiotic", .kind = .property, .links = &.{} },
.{ .id = 43, .name = "Alkalizing", .kind = .property, .links = &.{} },
.{ .id = 44, .name = "Enzyme Rich", .kind = .property, .links = &.{} },
};
const root_ids = [_]u8{ 0, 1, 2, 3, 4 };
fn findNode(id: u8) ?*const NodeDef {
for (&all_nodes) |*n| {
if (n.id == id) return n;
}
return null;
}
fn kindColor(kind: NodeKind) rl.Color {
fn kindColor(kind: data.Type) rl.Color {
return switch (kind) {
.body_part => rl.Color.init(70, 150, 215, 255),
.ailment => rl.Color.init(210, 75, 65, 255),
.recipe => rl.Color.init(75, 190, 110, 255),
.ingredient => rl.Color.init(215, 170, 50, 255),
.property => rl.Color.init(170, 90, 215, 255),
};
}
// ── Navigation state ──────────────────────────────────────────────────────────
const VisNode = struct { id: u8, x: f32, y: f32, is_center: bool };
const VisNode = struct { id: data.NodeID, x: f32, y: f32, is_center: bool };
const MAX_STACK = 16;
const MAX_VIS = 24;
var nav_stack: [MAX_STACK]u8 = undefined;
var nav_depth: usize = 0;
var nav_stack: [MAX_STACK]data.NodeID = undefined;
var vis: [MAX_VIS]VisNode = undefined;
var vis_count: usize = 0;
var hovered_id: ?u8 = null;
var hovered_id: ?data.NodeID = null;
var ready: bool = false;
fn buildVis() void {
vis_count = 0;
if (nav_depth == 0) {
// Root view: body systems arranged in a circle
const n = root_ids.len;
const r: f32 = 200.0;
for (root_ids, 0..) |id, i| {
const angle = (2.0 * math.pi * @as(f32, @floatFromInt(i))) /
@as(f32, @floatFromInt(n)) - math.pi / 2.0;
vis[vis_count] = .{
.id = id,
.x = centerX() + r * @cos(angle),
.y = centerY() + r * @sin(angle),
.is_center = false,
};
vis_count += 1;
}
} else {
// Drill-down view: selected node at centre, its links in orbit
const cur_id = nav_stack[nav_depth - 1];
const cur = findNode(cur_id) orelse return;
vis[vis_count] = .{ .id = cur_id, .x = centerX(), .y = centerY(), .is_center = true };
// Root view: body systems arranged in a circle
const r: f32 = 200.0;
for (std.enums.values(data.NodeID), 0..) |id, i| {
const angle = (2.0 * math.pi * @as(f32, @floatFromInt(i))) /
@as(f32, @floatFromInt(std.enums.values(data.NodeID).len)) - math.pi / 2.0;
vis[vis_count] = .{
.id = id,
.x = centerX() + r * @cos(angle),
.y = centerY() + r * @sin(angle),
.is_center = false,
};
vis_count += 1;
const n = cur.links.len;
if (n > 0) {
for (cur.links, 0..) |link_id, i| {
const angle = (2.0 * math.pi * @as(f32, @floatFromInt(i))) /
@as(f32, @floatFromInt(n)) - math.pi / 2.0;
vis[vis_count] = .{
.id = link_id,
.x = centerX() + ORBIT_R * @cos(angle),
.y = centerY() + ORBIT_R * @sin(angle),
.is_center = false,
};
vis_count += 1;
}
}
}
}
fn visNodeAt(mx: f32, my: f32) ?u8 {
fn visNodeAt(mx: f32, my: f32) ?data.NodeID {
for (vis[0..vis_count]) |vn| {
const dx = mx - vn.x;
const dy = my - vn.y;
@@ -160,29 +77,11 @@ fn update() void {
const mx: f32 = @floatFromInt(rl.getMouseX());
const my: f32 = @floatFromInt(rl.getMouseY());
hovered_id = visNodeAt(mx, my);
if (rl.isMouseButtonPressed(.left)) {
if (hovered_id) |id| {
const is_cur = nav_depth > 0 and nav_stack[nav_depth - 1] == id;
if (!is_cur and nav_depth < MAX_STACK) {
nav_stack[nav_depth] = id;
nav_depth += 1;
buildVis();
}
}
}
if (rl.isMouseButtonPressed(.right) or rl.isKeyPressed(.b)) {
if (nav_depth > 0) {
nav_depth -= 1;
buildVis();
}
}
}
fn drawScene() void {
// Edges from centre to orbit
if (nav_depth > 0 and vis_count > 1) {
if (vis_count > 1) {
const c = vis[0];
for (vis[1..vis_count]) |vn| {
rl.drawLineEx(
@@ -196,8 +95,8 @@ fn drawScene() void {
// Nodes
for (vis[0..vis_count]) |vn| {
const node = findNode(vn.id) orelse continue;
const col = kindColor(node.kind);
const node = data.nodes.get(vn.id);
const col = kindColor(node.@"type");
const hov = hovered_id != null and hovered_id.? == vn.id;
const r = if (hov) NODE_R + 5.0 else NODE_R;
const ix: i32 = @intFromFloat(vn.x);
@@ -229,28 +128,12 @@ fn drawHUD() void {
const by: i32 = 10;
rl.drawText("Body Systems", bx, by, 20, rl.Color.gray);
bx += rl.measureText("Body Systems", 20);
for (nav_stack[0..nav_depth]) |id| {
rl.drawText(" > ", bx, by, 20, rl.Color.dark_gray);
bx += rl.measureText(" > ", 20);
const node = findNode(id) orelse continue;
rl.drawText(node.name, bx, by, 20, rl.Color.ray_white);
bx += rl.measureText(node.name, 20);
}
// Navigation hint at bottom
const hint: [:0]const u8 = if (nav_depth > 0)
"[B] / right-click to go back"
else
"Click a body system to explore";
rl.drawText(hint, 10, screenH() - 28, 18, rl.Color.dark_gray);
// Colour legend (top-right)
const legend = [_]struct { kind: NodeKind, label: [:0]const u8 }{
.{ .kind = .body_part, .label = "Body System" },
const legend = [_]struct { kind: data.Type, label: [:0]const u8 }{
.{ .kind = .ailment, .label = "Ailment" },
.{ .kind = .recipe, .label = "Recipe" },
.{ .kind = .ingredient, .label = "Ingredient" },
.{ .kind = .property, .label = "Property" },
};
const lx: i32 = screenW() - 140;
var ly: i32 = 10;