avatar to main merge

I have completed the character creation screen on the avatar branch and am preparing to merge it into main.
This commit is contained in:
pricewowen
2026-05-23 23:24:36 -06:00
parent 8957ecb81d
commit 61f98866e8
39 changed files with 2361 additions and 436 deletions

View File

@@ -1,45 +1,11 @@
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
import { getCoins, spendCoins, adjustCoins } from "@/lib/avatar-progression-storage";
import { adjustCoins, getCoins } from "@/lib/avatar-progression-storage";
import {
parseVariantId,
type ShopItemRecord,
} from "@/lib/shop-catalog";
import { createClient } from "@/lib/supabase/client";
export type ShopItem = {
id: string;
name: string;
type: string;
price: number;
};
type ShopItemRow = {
id: string;
name: string;
type: string;
};
function getPriceForType(type: string) {
switch (type.toLowerCase()) {
case "accessory":
return 30;
case "background":
return 40;
case "outfit":
return 50;
case "consumable":
case "snack":
return 25;
default:
return 50;
}
}
function mapShopItemRow(row: ShopItemRow): ShopItem {
return {
id: row.id,
name: row.name,
type: row.type,
price: getPriceForType(row.type),
};
}
async function getAuthenticatedUserId() {
const supabase = createClient();
const {
@@ -54,19 +20,18 @@ async function getAuthenticatedUserId() {
return user.id;
}
export async function getShopItems(): Promise<ShopItem[]> {
export async function getShopItems(): Promise<ShopItemRecord[]> {
const supabase = createClient();
const { data, error } = await supabase
.from("shop_items")
.select("id, name, type")
.order("type", { ascending: true })
.order("name", { ascending: true });
.select("id, name, type, price, image_path")
.order("price", { ascending: true });
if (error) {
throw new Error(error.message);
}
return ((data ?? []) as ShopItemRow[]).map(mapShopItemRow);
return (data ?? []) as ShopItemRecord[];
}
export async function getOwnedItemIds(): Promise<string[]> {
@@ -89,7 +54,28 @@ export async function getOwnedItemIds(): Promise<string[]> {
return (data ?? []).map((row) => row.item_id as string);
}
export async function purchaseShopItem(itemId: string): Promise<number> {
export async function getEquippedItems(): Promise<string[]> {
const userId = await getAuthenticatedUserId();
if (!userId) {
return [];
}
const supabase = createClient();
const { data, error } = await supabase
.from("avatar_state")
.select("equipped_items")
.eq("user_id", userId)
.maybeSingle();
if (error) {
throw new Error(error.message);
}
return (data?.equipped_items as string[] | null) ?? [];
}
export async function purchaseShopItem(itemId: string) {
const userId = await getAuthenticatedUserId();
if (!userId) {
@@ -97,9 +83,9 @@ export async function purchaseShopItem(itemId: string): Promise<number> {
}
const supabase = createClient();
const { data: itemRow, error: itemError } = await supabase
const { data: item, error: itemError } = await supabase
.from("shop_items")
.select("id, name, type")
.select("id, name, type, price, image_path")
.eq("id", itemId)
.maybeSingle();
@@ -107,28 +93,23 @@ export async function purchaseShopItem(itemId: string): Promise<number> {
throw new Error(itemError.message);
}
if (!itemRow) {
throw new Error("Item not found.");
if (!item) {
throw new Error("That shop item is no longer available.");
}
const item = mapShopItemRow(itemRow as ShopItemRow);
const owned = await getOwnedItemIds();
const { data: existingPurchase, error: existingError } = await supabase
.from("user_items")
.select("item_id")
.eq("user_id", userId)
.eq("item_id", itemId)
.maybeSingle();
if (existingError) {
throw new Error(existingError.message);
}
if (existingPurchase) {
if (owned.includes(itemId)) {
throw new Error("You already own this item.");
}
const remainingCoins = await spendCoins(item.price);
const coins = await getCoins();
if (coins < item.price) {
throw new Error("Not enough points for this purchase.");
}
await adjustCoins(-item.price);
const { error: purchaseError } = await supabase.from("user_items").insert({
user_id: userId,
@@ -141,19 +122,106 @@ export async function purchaseShopItem(itemId: string): Promise<number> {
}
notifyHabitPetDataUpdated();
return remainingCoins;
return {
...(item as ShopItemRecord),
type: item.type as ShopItemRecord["type"],
};
}
export async function getShopState(): Promise<{
coins: number;
items: ShopItem[];
ownedItemIds: string[];
}> {
const [coins, items, ownedItemIds] = await Promise.all([
getCoins(),
export async function equipShopItem(itemId: string) {
const userId = await getAuthenticatedUserId();
if (!userId) {
throw new Error("You must be signed in to equip items.");
}
const owned = await getOwnedItemIds();
if (!owned.includes(itemId)) {
throw new Error("Purchase this item before equipping it.");
}
const supabase = createClient();
const { data: item, error: itemError } = await supabase
.from("shop_items")
.select("id, type")
.eq("id", itemId)
.maybeSingle();
if (itemError || !item) {
throw new Error("Could not find that shop item.");
}
const current = await getEquippedItems();
const nextStyle = parseVariantId(itemId);
const withoutSameLayer = current.filter((equippedId) => {
if (equippedId === itemId) {
return false;
}
const equippedStyle = parseVariantId(equippedId);
if (!nextStyle || !equippedStyle) {
return true;
}
return equippedStyle.layerId !== nextStyle.layerId;
});
const next = [...withoutSameLayer, itemId];
const { error } = await supabase
.from("avatar_state")
.update({
equipped_items: next,
equipped_item: itemId,
updated_at: new Date().toISOString(),
})
.eq("user_id", userId);
if (error) {
throw new Error(error.message);
}
notifyHabitPetDataUpdated();
return next;
}
export async function unequipShopItem(itemId: string) {
const userId = await getAuthenticatedUserId();
if (!userId) {
throw new Error("You must be signed in to unequip items.");
}
const current = await getEquippedItems();
const next = current.filter((id) => id !== itemId);
const supabase = createClient();
const { error } = await supabase
.from("avatar_state")
.update({
equipped_items: next,
equipped_item: next[0] ?? "none",
updated_at: new Date().toISOString(),
})
.eq("user_id", userId);
if (error) {
throw new Error(error.message);
}
notifyHabitPetDataUpdated();
return next;
}
export async function getShopInventory() {
const [items, ownedItemIds, equippedItems, coins] = await Promise.all([
getShopItems(),
getOwnedItemIds(),
getEquippedItems(),
getCoins(),
]);
return { coins, items, ownedItemIds };
return { items, ownedItemIds, equippedItems, coins };
}