//! Original code-native pixel companions. Pure, bounded geometry and a 10 Hz //! animation clock: no textures, allocation, input handling or editor state. const std = @import("std"); pub const Kind = enum { off, cat, frog }; pub const width = 12; pub const height = 9; pub const max_vertices = width * height * 6; pub const Lane = struct { left: f32, right: f32, top: f32, scale: f32 }; pub const Pose = struct { x: f32, y: f32, scale: f32, left: bool, walking: bool, frame: u8 }; /// Only the trailing blank part of the global tag is eligible. The caller /// supplies its last occupied cell, including any wide-grapheme continuation. pub fn lane(occupied: usize, cell_w: f32, screen_w: f32, top: f32, band_h: f32, editing: bool) ?Lane { if (editing or cell_w <= 0 or band_h < height + 2) return null; const scale = @min(3, @floor((band_h - 2) / height)); const left = @as(f32, @floatFromInt(occupied + 2)) * cell_w; const right = screen_w - 2 * cell_w - width * scale; if (right - left < 4 * scale) return null; return .{ .left = left, .right = right, .top = top + band_h - height * scale - 1, .scale = scale }; } pub const State = struct { kind: Kind = .off, tick: ?u64 = null, x: f32 = 0, left: bool = false, idle: u8 = 12, frame: u8 = 0, pub fn sample(s: *State, kind: Kind, now_ms: u64, area: ?Lane) ?Pose { const region = area orelse { s.* = .{}; return null; }; if (kind == .off) { s.* = .{}; return null; } const tick = now_ms / 100; if (s.kind != kind or s.tick == null) { s.* = .{ .kind = kind, .tick = tick, .x = region.left }; } s.x = std.math.clamp(s.x, region.left, region.right); // Coming back from suspension never launches the pet across the tag. const steps = @min(tick -| s.tick.?, 2); s.tick = tick; for (0..steps) |_| { s.frame +%= 1; if (s.idle > 0) { s.idle -= 1; continue; } const speed: f32 = if (kind == .cat) 1.8 else 1.3; s.x += (if (s.left) -speed else speed) * region.scale; if (s.x >= region.right or s.x <= region.left) { s.x = std.math.clamp(s.x, region.left, region.right); s.left = !s.left; s.idle = if (kind == .cat) 18 else 24; } } return .{ .x = @floor(s.x), .y = region.top, .scale = region.scale, .left = s.left, .walking = s.idle == 0, .frame = s.frame }; } }; const cat = [_][]const u8{ " # # ", " #####", " ##o##", " # ######", " # ######## ", " ######### ", " ######## ", " ## ## ", " # # ", }; const frog = [_][]const u8{ " ", " ## ## ", " #o####o# ", " ######## ", " ###### ", " ######## ", " ########## ", " ## ## ", "### ###", }; /// 0 transparent, 1 theme ink, 2 eye cutout. Walk legs and idle blinks are /// generated in this fixed 12x9 tile; neither can spill into an adjacent row. pub fn pixel(kind: Kind, pose: Pose, x_in: usize, y: usize) u2 { if (x_in >= width or y >= height or kind == .off) return 0; const x = if (pose.left) width - 1 - x_in else x_in; const alternate = (pose.frame / 2) % 2 == 1; // An idle companion still looks alive during its first pause: a slow // two-pixel tail-tip flick, or the frog's throat gently inflating. These // remain inside the tile and mirror with the rest of the animal. const breathing = (pose.frame / 4) % 2 == 1; if (!pose.walking and breathing) { if (kind == .cat and y == 3 and x <= 1) return @intFromBool(x == 0); if (kind == .frog and y == 4 and (x == 2 or x == 9)) return 1; } if (pose.walking and alternate and y >= 7) { if (kind == .cat) return @intFromBool((y == 7 and (x == 3 or x == 7)) or (y == 8 and (x == 2 or x == 8))); return @intFromBool((y == 7 and (x == 2 or x == 9)) or (y == 8 and (x <= 2 or x >= 9))); } const ch = (if (kind == .cat) cat else frog)[y][x]; return switch (ch) { '#' => 1, 'o' => if (!pose.walking and pose.frame % 30 >= 27) 1 else 2, else => 0, }; } test "pet stays in trailing workspace blanks and disappears during editing" { try std.testing.expect(lane(20, 8, 200, 0, 16, false) == null); try std.testing.expect(lane(3, 8, 200, 0, 16, true) == null); try std.testing.expect(lane(3, 8, 200, 0, 10, false) == null); const region = lane(3, 8, 200, 2, 20, false).?; try std.testing.expect(region.left >= 5 * 8); var state: State = .{}; var moved = false; var reversed = false; for (0..2000) |i| { const pose = state.sample(.cat, i * 100, region).?; try std.testing.expect(pose.x >= region.left and pose.x + width * pose.scale <= 200 - 16); try std.testing.expect(pose.y >= 2 and pose.y + height * pose.scale < 22); moved = moved or pose.walking; reversed = reversed or pose.left; } try std.testing.expect(moved and reversed); try std.testing.expect(state.sample(.cat, 300000, null) == null); try std.testing.expect(state.sample(.off, 300100, region) == null); } test "pet clock is fixed rate and sprites bounded in both directions" { const region = lane(1, 8, 400, 0, 16, false).?; var state: State = .{}; const start = state.sample(.frog, 1000, region).?; try std.testing.expectEqualDeep(start, state.sample(.frog, 1099, region).?); for ([_]Kind{ .cat, .frog }) |kind| for ([_]bool{ false, true }) |left| { const pose: Pose = .{ .x = 0, .y = 0, .scale = 1, .left = left, .walking = true, .frame = 3 }; var count: usize = 0; for (0..height) |y| for (0..width) |x| { count += @intFromBool(pixel(kind, pose, x, y) != 0); }; try std.testing.expect(count > 20 and count <= width * height); try std.testing.expectEqual(@as(u2, 0), pixel(kind, pose, width, height)); }; } test "pet initial idle visibly animates without walking or leaving its tile" { const region = lane(1, 8, 400, 0, 16, false).?; for ([_]Kind{ .cat, .frog }) |kind| { var state: State = .{}; const initial = state.sample(kind, 0, region).?; var later = initial; for (1..5) |tick| later = state.sample(kind, tick * 100, region).?; try std.testing.expect(!initial.walking and !later.walking); try std.testing.expectEqual(initial.x, later.x); try std.testing.expectEqual(initial.y, later.y); var changes: usize = 0; for (0..height) |y| for (0..width) |x| { changes += @intFromBool(pixel(kind, initial, x, y) != pixel(kind, later, x, y)); }; try std.testing.expectEqual(@as(usize, 2), changes); try std.testing.expectEqual(@as(u2, 0), pixel(kind, later, width, height)); } }