- {showNameAboveShell ? (
+ {showStaticNameAboveShell ? (
@@ -127,14 +135,16 @@ export function PetHabitat({
-
+
+
+
- {showNameInLcdOverlay ? (
+ {showFloatingEditableName ? (
+
+ ) : null}
+ {showReadOnlyLcdName ? (
(null);
const [isResettingQuiz, setIsResettingQuiz] = useState(false);
+ const [coinsGrantMessage, setCoinsGrantMessage] = useState(null);
+ const [isGrantingCoins, setIsGrantingCoins] = useState(false);
useEffect(() => {
setNotificationPermission(getNotificationPermissionState());
@@ -212,6 +217,28 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
);
};
+ const handleGrantDemoCoins = async () => {
+ setCoinsGrantMessage(null);
+ setError(null);
+ setIsGrantingCoins(true);
+
+ try {
+ const nextBalance = await adjustCoins(DEMO_COINS_GRANT);
+ notifyHabitPetDataUpdated();
+ setCoinsGrantMessage(
+ `Added ${DEMO_COINS_GRANT} points. Your balance is now ${nextBalance}.`,
+ );
+ } catch (grantError) {
+ setError(
+ grantError instanceof Error
+ ? grantError.message
+ : "Could not add demo points.",
+ );
+ } finally {
+ setIsGrantingCoins(false);
+ }
+ };
+
const handleResetDailyQuiz = async () => {
setQuizResetMessage(null);
setError(null);
@@ -399,8 +426,22 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
Reset clears today's wellness quiz entry so you can complete it
- again immediately.
+ again immediately. Demo points are for testing the shop only.
+
+ {coinsGrantMessage ? (
+ {coinsGrantMessage}
+ ) : null}
@@ -355,6 +364,7 @@ export function ShopContent() {
equipped={equippedRoomBackground === item.id}
canAfford={coins !== null && coins >= item.price}
isPending={pendingItemId === item.id}
+ characterColors={baseCustomization.colors}
onPurchase={() => void handlePurchase(item.id)}
onEquipToggle={() => void handleEquipToggle(item)}
onPreview={() => setPreviewItem(item)}
@@ -378,6 +388,7 @@ export function ShopContent() {
equipped={equippedItems.includes(item.id)}
canAfford={coins !== null && coins >= item.price}
isPending={pendingItemId === item.id}
+ characterColors={baseCustomization.colors}
onPurchase={() => void handlePurchase(item.id)}
onEquipToggle={() => void handleEquipToggle(item)}
onPreview={() => setPreviewItem(item)}
diff --git a/lib/coins.ts b/lib/coins.ts
index 30d4840..339ef80 100644
--- a/lib/coins.ts
+++ b/lib/coins.ts
@@ -1,5 +1,8 @@
export const DEFAULT_STARTING_COINS = 100;
+/** Demo grant amount from Profile → Demo tools. */
+export const DEMO_COINS_GRANT = 100;
+
export const COINS_PER_TASK_COMPLETE = 50;
export const COINS_PER_TASK_UNCHECK = -50;
diff --git a/lib/pet-wander-bounds.ts b/lib/pet-wander-bounds.ts
index 23e4863..89f5f5e 100644
--- a/lib/pet-wander-bounds.ts
+++ b/lib/pet-wander-bounds.ts
@@ -1,14 +1,17 @@
-/** Percentage bounds for pet wander within the LCD viewport. */
+/** Ground band height as a fraction of the LCD — matches room PNG floor (~bottom 22%). */
+export const PET_GROUND_ZONE_HEIGHT_RATIO = 0.22;
+
+/** Wander bounds inside the ground zone (not the full LCD). */
export const PET_WANDER_BOUNDS = {
/** Horizontal inset — keep pet off side walls */
minX: 15,
maxX: 85,
- /** Lower third of LCD — ground where hills/grass/floor render */
- minY: 66,
- maxY: 90,
+ /** Vertical offset from floor, as % of ground zone height (0 = feet on floor) */
+ minY: 0,
+ maxY: 65,
} as const;
export const PET_WANDER_START = {
x: 50,
- y: 78,
+ y: 12,
} as const;
diff --git a/lib/shop-preview.ts b/lib/shop-preview.ts
index aa31e9a..08eb29f 100644
--- a/lib/shop-preview.ts
+++ b/lib/shop-preview.ts
@@ -1,5 +1,12 @@
import type { AvatarCustomization } from "@/lib/avatar-customization-storage";
-import { parseVariantId, parseShopStyleItem, type ShopItemRecord } from "@/lib/shop-catalog";
+import type { HSL } from "@/lib/character/color-utils";
+import type { LayerColorState } from "@/lib/character/types";
+import {
+ isShopLayerId,
+ parseVariantId,
+ parseShopStyleItem,
+ type ShopItemRecord,
+} from "@/lib/shop-catalog";
import {
normalizeRoomBackgroundId,
type RoomBackgroundId,
@@ -17,6 +24,21 @@ export function getShopPreviewRoomId(
return normalizeRoomBackgroundId(base.roomBackground);
}
+/** Tint colors for shop grid thumbnails — match the user's current layer colors. */
+export function getShopItemThumbnailColors(
+ item: ShopItemRecord,
+ colors: LayerColorState,
+): { color: HSL; skinColor?: HSL } {
+ if (!isShopLayerId(item.type)) {
+ return { color: colors.torso };
+ }
+
+ return {
+ color: colors[item.type],
+ skinColor: item.type === "eyes" ? colors.skin : undefined,
+ };
+}
+
/** Build a non-persisted avatar state showing how a shop item would look. */
export function buildShopItemPreviewCustomization(
base: AvatarCustomization,