-
Price
-
{item.price} points
+
+
+
+ {item.price}
+
+ points
{owned ? (
diff --git a/lib/ai-prompt-security.ts b/lib/ai-prompt-security.ts
index 251990f..edee108 100644
--- a/lib/ai-prompt-security.ts
+++ b/lib/ai-prompt-security.ts
@@ -1,4 +1,4 @@
-export const AI_TASK_SYSTEM_PROMPT = `You are a supportive wellness coach for a habit-tracking pet app.
+export const AI_TASK_SYSTEM_PROMPT = `You are a supportive wellness coach for a habit-tracking app where users care for a digital self (their "bit").
Your only allowed action is to suggest practical daily habit task labels based on trusted app context and untrusted user journal text.
diff --git a/lib/ai-task-context.ts b/lib/ai-task-context.ts
index 7a9fbeb..b77b215 100644
--- a/lib/ai-task-context.ts
+++ b/lib/ai-task-context.ts
@@ -60,7 +60,7 @@ export function formatAiTaskContextForPrompt(context: AiTaskContext) {
`Daily reminder enabled: ${preferences.dailyReminderEnabled ? "yes" : "no"}`,
`Daily reminder time: ${preferences.dailyReminderTime}`,
`Primary focus: ${onboarding.focusTopic ?? "unknown"}`,
- `Starter pet vibe: ${onboarding.avatarVibe ?? "unknown"}`,
+ `Starter bit vibe: ${onboarding.avatarVibe ?? "unknown"}`,
].join("\n"),
);
diff --git a/lib/avatar-customization-storage.ts b/lib/avatar-customization-storage.ts
index 450a90a..0662620 100644
--- a/lib/avatar-customization-storage.ts
+++ b/lib/avatar-customization-storage.ts
@@ -51,7 +51,7 @@ type AvatarStateRow = {
room_background: string | null;
};
-export const DEFAULT_AVATAR_NAME = "Pixel Me";
+export const DEFAULT_AVATAR_NAME = "My Bit";
const DEFAULT_HEX = DEFAULT_GRAY_COLOR.hex;
const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
diff --git a/lib/brand.ts b/lib/brand.ts
new file mode 100644
index 0000000..9805614
--- /dev/null
+++ b/lib/brand.ts
@@ -0,0 +1 @@
+export const HABIT_LOGO_SRC = "/habit-logo.png";
diff --git a/lib/daily-reminders.ts b/lib/daily-reminders.ts
index 8462a86..761a2d4 100644
--- a/lib/daily-reminders.ts
+++ b/lib/daily-reminders.ts
@@ -129,7 +129,7 @@ export function buildDailyReminderNotification(status: DailyReminderStatus) {
if (!status.quizCompleted) {
return {
title: "Time for your daily check-in",
- body: "Complete today's wellness quiz to keep your pet happy.",
+ body: "Complete today's wellness quiz to keep your bit in good spirits.",
targetUrl,
};
}
@@ -174,7 +174,7 @@ export function showTestSystemDailyReminderNotification(
}
const { body, targetUrl } = buildDailyReminderNotification(status);
- const notification = new Notification("Test: Habit Pet reminder", {
+ const notification = new Notification("Test: HaBit reminder", {
body: `This is a browser test notification. ${body}`,
tag: `habit-pet-daily-reminder-test-${Date.now()}`,
});
diff --git a/lib/landing-random-pet.ts b/lib/landing-random-pet.ts
index 4be9491..76fff6f 100644
--- a/lib/landing-random-pet.ts
+++ b/lib/landing-random-pet.ts
@@ -1,43 +1,45 @@
-import {
- DEFAULT_AVATAR_NAME,
- type AvatarCustomization,
-} from "@/lib/avatar-customization-storage";
+import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
import {
CHARACTER_LAYERS,
COLOR_PRESETS,
NONE_VARIANT_ID,
type CharacterLayerId,
} from "@/lib/character/presets";
-import { ROOM_BACKGROUNDS } from "@/lib/room-backgrounds";
+import { clampHsl } from "@/lib/character/color-utils";
+import {
+ ROOM_BACKGROUNDS,
+ type RoomBackgroundId,
+} from "@/lib/room-backgrounds";
const GUEST_PET_STORAGE_KEY = "habit-pet-guest-landing-pet";
-const SKIN_TONE_IDS = [
- "porcelain",
- "fair",
- "light",
- "medium",
- "tan",
- "brown",
- "dark",
- "deep",
- "onyx",
+export const GUEST_LANDING_BIT_NAMES = [
+ "Alex",
+ "Jordan",
+ "Sam",
+ "Taylor",
+ "Morgan",
] as const;
-const OUTFIT_COLOR_IDS = [
- "red",
- "orange",
- "yellow",
- "green",
- "blue",
- "indigo",
- "black",
- "white",
-] as const;
+const SKIN_COLOR_PRESETS = COLOR_PRESETS.filter((preset) =>
+ [
+ "porcelain",
+ "fair",
+ "light",
+ "medium",
+ "tan",
+ "brown",
+ "dark",
+ "deep",
+ "onyx",
+ ].includes(preset.id),
+);
-const EYE_COLOR_IDS = ["blue", "black", "green", "brown"] as const;
+const STYLE_COLOR_PRESETS = COLOR_PRESETS.filter(
+ (preset) => !SKIN_COLOR_PRESETS.some((skin) => skin.id === preset.id),
+);
-const HAIR_COLOR_IDS = ["brown", "black", "yellow", "red", "orange", "white"] as const;
+const ALL_ROOM_IDS = ROOM_BACKGROUNDS.map((room) => room.id);
function pickRandom
(items: readonly T[]): T {
return items[Math.floor(Math.random() * items.length)]!;
@@ -52,48 +54,61 @@ function presetHsl(id: string) {
return { ...preset.hsl };
}
-function pickRandomVariant(
- layerId: Exclude,
- allowNone = false,
-): string {
+function randomHsl(): { h: number; s: number; l: number } {
+ return clampHsl({
+ h: Math.floor(Math.random() * 360),
+ s: 35 + Math.floor(Math.random() * 50),
+ l: 28 + Math.floor(Math.random() * 45),
+ });
+}
+
+function pickRandomLayerColor(layerId: CharacterLayerId) {
+ if (layerId === "skin") {
+ return presetHsl(pickRandom(SKIN_COLOR_PRESETS).id);
+ }
+
+ if (Math.random() < 0.35) {
+ return randomHsl();
+ }
+
+ return presetHsl(pickRandom(STYLE_COLOR_PRESETS).id);
+}
+
+function pickRandomLayerVariant(layerId: CharacterLayerId): string {
const layer = CHARACTER_LAYERS.find((entry) => entry.id === layerId);
if (!layer) {
throw new Error(`Missing character layer: ${layerId}`);
}
- const options = layer.variants
- .map((variant) => variant.id)
- .filter((id) => allowNone || id !== NONE_VARIANT_ID);
+ if (!layer.allowVariants) {
+ return layer.variants[0]?.id ?? NONE_VARIANT_ID;
+ }
- return pickRandom(options);
+ return pickRandom(layer.variants.map((variant) => variant.id));
}
export function buildRandomLandingPetCustomization(): AvatarCustomization {
- const freeRooms = ROOM_BACKGROUNDS.filter((room) => room.free).map(
- (room) => room.id,
- );
-
return {
- name: DEFAULT_AVATAR_NAME,
+ name: pickRandom(GUEST_LANDING_BIT_NAMES),
colors: {
- skin: presetHsl(pickRandom(SKIN_TONE_IDS)),
- pants: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
- shoes: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
- torso: presetHsl(pickRandom(OUTFIT_COLOR_IDS)),
- eyes: presetHsl(pickRandom(EYE_COLOR_IDS)),
- head: presetHsl(pickRandom(HAIR_COLOR_IDS)),
+ skin: pickRandomLayerColor("skin"),
+ pants: pickRandomLayerColor("pants"),
+ shoes: pickRandomLayerColor("shoes"),
+ torso: pickRandomLayerColor("torso"),
+ eyes: pickRandomLayerColor("eyes"),
+ head: pickRandomLayerColor("head"),
},
variants: {
- skin: "skin-1",
- pants: pickRandomVariant("pants"),
- shoes: pickRandomVariant("shoes"),
- torso: pickRandomVariant("torso"),
- eyes: pickRandomVariant("eyes"),
- head: pickRandomVariant("head", Math.random() < 0.15),
+ skin: pickRandomLayerVariant("skin"),
+ pants: pickRandomLayerVariant("pants"),
+ shoes: pickRandomLayerVariant("shoes"),
+ torso: pickRandomLayerVariant("torso"),
+ eyes: pickRandomLayerVariant("eyes"),
+ head: pickRandomLayerVariant("head"),
},
customized: true,
equippedItems: [],
- roomBackground: pickRandom(freeRooms),
+ roomBackground: pickRandom(ALL_ROOM_IDS) as RoomBackgroundId,
};
}
@@ -105,7 +120,14 @@ export function getGuestLandingPetCustomization(): AvatarCustomization {
try {
const cached = sessionStorage.getItem(GUEST_PET_STORAGE_KEY);
if (cached) {
- return JSON.parse(cached) as AvatarCustomization;
+ const parsed = JSON.parse(cached) as AvatarCustomization;
+ if (
+ GUEST_LANDING_BIT_NAMES.includes(
+ parsed.name as (typeof GUEST_LANDING_BIT_NAMES)[number],
+ )
+ ) {
+ return parsed;
+ }
}
const next = buildRandomLandingPetCustomization();
diff --git a/lib/openrouter.ts b/lib/openrouter.ts
index b312155..c7e9cc0 100644
--- a/lib/openrouter.ts
+++ b/lib/openrouter.ts
@@ -46,7 +46,7 @@ export async function generateAiTaskLabels(
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"HTTP-Referer": siteUrl,
- "X-Title": "Habit Pet",
+ "X-Title": "HaBit",
},
body: JSON.stringify({
model: DEFAULT_OPENROUTER_MODEL,
diff --git a/lib/pet-status.ts b/lib/pet-status.ts
new file mode 100644
index 0000000..0e19669
--- /dev/null
+++ b/lib/pet-status.ts
@@ -0,0 +1,26 @@
+import type { AvatarMood } from "@/lib/avatar-state";
+
+export const PET_EMOTION_DISPLAY_MS = 3200;
+
+export type PetStatusImageId = "status-1" | "status-2" | "status-3";
+
+export const PET_STATUS_BY_MOOD: Record = {
+ happy: "status-1",
+ neutral: "status-2",
+ tired: "status-3",
+};
+
+export function getPetStatusImagePath(mood: AvatarMood): string {
+ return `/status/${PET_STATUS_BY_MOOD[mood]}.png`;
+}
+
+export function getPetEmotionMessage(mood: AvatarMood): string {
+ switch (mood) {
+ case "happy":
+ return "Your bit seems cheerful and in good spirits.";
+ case "tired":
+ return "Your bit seems worn out and could use some care.";
+ default:
+ return "Your bit seems steady and doing okay.";
+ }
+}
diff --git a/lib/reminder-delivery.ts b/lib/reminder-delivery.ts
index 77d903e..a808d34 100644
--- a/lib/reminder-delivery.ts
+++ b/lib/reminder-delivery.ts
@@ -3,7 +3,7 @@ export const reminderDeliveryOptions = [
value: "in_app",
label: "In-app",
description:
- "Show reminders inside Habit Pet when you have the site open in a tab.",
+ "Show reminders inside HaBit when you have the site open in a tab.",
},
{
value: "system",
diff --git a/lib/routes.ts b/lib/routes.ts
index 3856cd7..01f5aa9 100644
--- a/lib/routes.ts
+++ b/lib/routes.ts
@@ -16,7 +16,8 @@ export const routes = {
export type AppRoute = (typeof routes)[keyof typeof routes];
export const appNavItems = [
- { href: routes.avatar, label: "Pet", shortLabel: "Pet" },
+ { href: routes.avatar, label: "Bit", shortLabel: "Bit" },
+ { href: routes.customize, label: "Customize", shortLabel: "Style" },
{ href: routes.habits, label: "Habits", shortLabel: "Habits" },
{ href: routes.dailyQuiz, label: "Daily Quiz", shortLabel: "Quiz" },
{ href: routes.shop, label: "Shop", shortLabel: "Shop" },
diff --git a/public/habit-logo.png b/public/habit-logo.png
new file mode 100644
index 0000000..6f8d425
Binary files /dev/null and b/public/habit-logo.png differ
diff --git a/public/status/status-1.png b/public/status/status-1.png
new file mode 100644
index 0000000..db08c3e
Binary files /dev/null and b/public/status/status-1.png differ
diff --git a/public/status/status-2.png b/public/status/status-2.png
new file mode 100644
index 0000000..b2181bd
Binary files /dev/null and b/public/status/status-2.png differ
diff --git a/public/status/status-3.png b/public/status/status-3.png
new file mode 100644
index 0000000..0f4a2ce
Binary files /dev/null and b/public/status/status-3.png differ