Removed the auto generated sprites
This commit is contained in:
16
README.md
16
README.md
@@ -38,32 +38,26 @@ On the **Pet** tab, users rename their pet and switch between **Overview** and *
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Generate placeholder character/shop sprites (if missing):
|
||||
|
||||
```bash
|
||||
npm run generate:sprites
|
||||
```
|
||||
|
||||
3. Create local environment file:
|
||||
2. Create local environment file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
4. Add Supabase values to `.env.local`:
|
||||
3. Add Supabase values to `.env.local`:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
```
|
||||
|
||||
5. Apply migrations locally (Supabase CLI):
|
||||
4. Apply migrations locally (Supabase CLI):
|
||||
|
||||
```bash
|
||||
supabase db reset
|
||||
```
|
||||
|
||||
6. Start the app:
|
||||
5. Start the app:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -87,7 +81,7 @@ Stored in `avatar_state`:
|
||||
|
||||
Style columns are validated against allowed variant ids (`none`, `pants-1`, …). Shop accessories overlay equipped variants at render time without overwriting saved customization.
|
||||
|
||||
Sprite assets: `public/character/{layer}/` (regenerate via `npm run generate:sprites`).
|
||||
Sprite assets are added manually under `public/character/{layer}/` (e.g. `public/character/head/head-1.png`). Paths must match the variant ids in `lib/character/presets.ts` and `supabase/seed.sql`.
|
||||
|
||||
## Shop
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
import type { LayerColorState, LayerVariantState } from "@/lib/character/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const EMPTY_EQUIPPED_ITEMS: string[] = [];
|
||||
|
||||
type CharacterLayerPreviewProps = {
|
||||
colors: LayerColorState;
|
||||
variants: LayerVariantState;
|
||||
@@ -27,16 +29,21 @@ type CharacterLayerPreviewProps = {
|
||||
export function CharacterLayerPreview({
|
||||
colors,
|
||||
variants,
|
||||
equippedItems = [],
|
||||
equippedItems,
|
||||
scale = 10,
|
||||
className,
|
||||
compact = false,
|
||||
}: CharacterLayerPreviewProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [displaySize, setDisplaySize] = useState({ width: 0, height: 0 });
|
||||
const resolvedEquippedItems = equippedItems ?? EMPTY_EQUIPPED_ITEMS;
|
||||
const equippedKey = resolvedEquippedItems.join("\0");
|
||||
const equippedItemsRef = useRef(resolvedEquippedItems);
|
||||
equippedItemsRef.current = resolvedEquippedItems;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const items = equippedItemsRef.current;
|
||||
|
||||
async function render() {
|
||||
const canvas = canvasRef.current;
|
||||
@@ -45,62 +52,74 @@ export function CharacterLayerPreview({
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!ctx) return;
|
||||
|
||||
const displayVariants = applyEquippedItemsToVariants(
|
||||
variants,
|
||||
equippedItems,
|
||||
);
|
||||
|
||||
const activeLayers = CHARACTER_LAYERS.flatMap((layer) => {
|
||||
const selected = getSelectedVariant(layer.id, displayVariants[layer.id]);
|
||||
if (!selected) return [];
|
||||
return [{ layer, variant: selected }];
|
||||
});
|
||||
|
||||
const images = await Promise.all(
|
||||
activeLayers.map(async ({ layer, variant }) => ({
|
||||
layer,
|
||||
image: await getCachedImage(variant.src),
|
||||
})),
|
||||
);
|
||||
|
||||
if (cancelled || images.length === 0) return;
|
||||
|
||||
const width = images[0]?.image.width ?? 0;
|
||||
const height = images[0]?.image.height ?? 0;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
setDisplaySize({ width: width * scale, height: height * scale });
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
for (const { layer, image } of images) {
|
||||
const layerColor = colors[layer.id];
|
||||
const rgb = hslToRgb(layerColor.h, layerColor.s, layerColor.l);
|
||||
|
||||
drawTintedSprite(
|
||||
ctx,
|
||||
image,
|
||||
rgb,
|
||||
layer.id === "eyes"
|
||||
? {
|
||||
mode: "eyes",
|
||||
skinColor: hslToRgb(
|
||||
colors.skin.h,
|
||||
colors.skin.s,
|
||||
colors.skin.l,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
try {
|
||||
const displayVariants = applyEquippedItemsToVariants(
|
||||
variants,
|
||||
items,
|
||||
);
|
||||
|
||||
const activeLayers = CHARACTER_LAYERS.flatMap((layer) => {
|
||||
const selected = getSelectedVariant(layer.id, displayVariants[layer.id]);
|
||||
if (!selected) return [];
|
||||
return [{ layer, variant: selected }];
|
||||
});
|
||||
|
||||
const images = await Promise.all(
|
||||
activeLayers.map(async ({ layer, variant }) => ({
|
||||
layer,
|
||||
image: await getCachedImage(variant.src),
|
||||
})),
|
||||
);
|
||||
|
||||
if (cancelled || images.length === 0) return;
|
||||
|
||||
const width = images[0]?.image.width ?? 0;
|
||||
const height = images[0]?.image.height ?? 0;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const nextWidth = width * scale;
|
||||
const nextHeight = height * scale;
|
||||
setDisplaySize((current) =>
|
||||
current.width === nextWidth && current.height === nextHeight
|
||||
? current
|
||||
: { width: nextWidth, height: nextHeight },
|
||||
);
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
for (const { layer, image } of images) {
|
||||
const layerColor = colors[layer.id];
|
||||
const rgb = hslToRgb(layerColor.h, layerColor.s, layerColor.l);
|
||||
|
||||
drawTintedSprite(
|
||||
ctx,
|
||||
image,
|
||||
rgb,
|
||||
layer.id === "eyes"
|
||||
? {
|
||||
mode: "eyes",
|
||||
skinColor: hslToRgb(
|
||||
colors.skin.h,
|
||||
colors.skin.s,
|
||||
colors.skin.l,
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing or broken sprite assets so the UI stays responsive.
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
void render();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [colors, variants, equippedItems, scale]);
|
||||
}, [colors, variants, equippedKey, scale]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -37,35 +37,39 @@ export function TintedSpriteIcon({
|
||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
if (!ctx) return;
|
||||
|
||||
const image = await getCachedImage(src);
|
||||
if (cancelled) return;
|
||||
try {
|
||||
const image = await getCachedImage(src);
|
||||
if (cancelled) return;
|
||||
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
ctx.clearRect(0, 0, image.width, image.height);
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
ctx.clearRect(0, 0, image.width, image.height);
|
||||
|
||||
const rgb = hslToRgb(color.h, color.s, color.l);
|
||||
const isEyesLayer = src.includes("/character/eyes/");
|
||||
const rgb = hslToRgb(color.h, color.s, color.l);
|
||||
const isEyesLayer = src.includes("/character/eyes/");
|
||||
|
||||
drawTintedSprite(
|
||||
ctx,
|
||||
image,
|
||||
rgb,
|
||||
isEyesLayer && skinColor
|
||||
? {
|
||||
mode: "eyes",
|
||||
skinColor: hslToRgb(skinColor.h, skinColor.s, skinColor.l),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
drawTintedSprite(
|
||||
ctx,
|
||||
image,
|
||||
rgb,
|
||||
isEyesLayer && skinColor
|
||||
? {
|
||||
mode: "eyes",
|
||||
skinColor: hslToRgb(skinColor.h, skinColor.s, skinColor.l),
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
} catch {
|
||||
// Ignore missing or broken sprite assets so the UI stays responsive.
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
void render();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [src, color, skinColor]);
|
||||
}, [src, color.h, color.s, color.l, skinColor?.h, skinColor?.s, skinColor?.l]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { CharacterLayerPreview } from "@/components/character/character-layer-preview";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { defaultAvatarCustomization } from "@/lib/avatar-customization-storage";
|
||||
import {
|
||||
focusTopicOptions,
|
||||
saveOnboardingPreferences,
|
||||
@@ -52,25 +50,16 @@ export function OnboardingQuizForm() {
|
||||
return (
|
||||
<div className="min-h-svh bg-background">
|
||||
<main className="mx-auto flex min-h-svh max-w-lg flex-col justify-center px-5 py-10">
|
||||
<div className="mb-8 flex flex-col items-center gap-4 text-center">
|
||||
<CharacterLayerPreview
|
||||
colors={defaultAvatarCustomization.colors}
|
||||
variants={defaultAvatarCustomization.variants}
|
||||
scale={6}
|
||||
compact
|
||||
className="max-w-[12rem]"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.2em] text-muted-foreground">
|
||||
Step 1 of 2
|
||||
</p>
|
||||
<h1 className="mt-2 text-2xl font-semibold tracking-tight">
|
||||
Welcome to Habit Pet
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Choose a focus area, then design your pet in the next step.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-8 text-center">
|
||||
<p className="text-sm uppercase tracking-[0.2em] text-muted-foreground">
|
||||
Step 1 of 2
|
||||
</p>
|
||||
<h1 className="mt-2 text-2xl font-semibold tracking-tight">
|
||||
Welcome to Habit Pet
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Choose a focus area, then design your pet in the next step.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
|
||||
321
package-lock.json
generated
321
package-lock.json
generated
@@ -10,8 +10,8 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@supabase/ssr": "^0.10.3",
|
||||
"@supabase/supabase-js": "^2.106.1",
|
||||
"@supabase/ssr": "latest",
|
||||
"@supabase/supabase-js": "latest",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
@@ -29,10 +29,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.1",
|
||||
"pg": "^8.16.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8",
|
||||
"supabase": "^2.101.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5",
|
||||
@@ -2955,130 +2952,6 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/cli-darwin-arm64": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/cli-darwin-arm64/-/cli-darwin-arm64-2.101.0.tgz",
|
||||
"integrity": "sha512-SoWYzu2CIE+rADWsZH6H0YZYF8nV2YMelZLbjAqzahb5RRgFOTMWGhKCUj6g4KlNvocUnU19JObUJ7oCx/9aHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/cli-darwin-x64": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/cli-darwin-x64/-/cli-darwin-x64-2.101.0.tgz",
|
||||
"integrity": "sha512-rPES03BF9KYkirq3X1Yll+T+eORueRvC858Yp2+2aQMvB4qxi6k+WI+ilCQ7NGzOJ3G0jl/ZG/KsSjZIa4AJ6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/cli-linux-arm64": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64/-/cli-linux-arm64-2.101.0.tgz",
|
||||
"integrity": "sha512-E+hN1GRIFEi+U1Jf4Omw/gjwpSRTpJ5jai58tQana15AaXt3GCoG7z2YzWVnzD/mvAubTqA7SLHsV+A2lcdTGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/cli-linux-arm64-musl": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.101.0.tgz",
|
||||
"integrity": "sha512-B5JFFKKpJLLMwn8vWUB7j5euZPytiDOLo9WYXp0mshg45OaFKjRcDImSzBzx4k0W8MK2x75rFEURca90Ic60Ug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/cli-linux-x64": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64/-/cli-linux-x64-2.101.0.tgz",
|
||||
"integrity": "sha512-7iU9uA+8pbmW5uz+yGxU0fM83a+8SH/gtKmSSRZrOu9vIGcugg9gLbN9GlWkG5P7I7wEjnoVKb7Jzea6onSeQA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/cli-linux-x64-musl": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64-musl/-/cli-linux-x64-musl-2.101.0.tgz",
|
||||
"integrity": "sha512-pr9Ar1/aVaQVqjNSfwRgXHWJznQTIaxblGO+hKIl9k55Ajahiq8GConCwx6DjX1Wa9VW//5Dji+O0pkmFSfcHw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/cli-windows-arm64": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/cli-windows-arm64/-/cli-windows-arm64-2.101.0.tgz",
|
||||
"integrity": "sha512-mSnFDhTIAe9cr1iifv+DTQzpY/0WphywED2YT3yMGeu94jjxrXrreQkywxEaeSXTnc4ys1umtnWOB1CaBlYKag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/cli-windows-x64": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/cli-windows-x64/-/cli-windows-x64-2.101.0.tgz",
|
||||
"integrity": "sha512-xzZpybVWq62VF102abZr1EP3BbA2bYecwYm3G2dJk+6ua7nAIodu1fiGHAvBSR0BLGQmesNb6KCneawNL3XBsQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.106.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.106.1.tgz",
|
||||
@@ -9024,103 +8897,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
|
||||
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.13.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
"pg-protocol": "^1.14.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
|
||||
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
|
||||
"integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
||||
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
|
||||
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -9160,16 +8936,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
@@ -9365,49 +9131,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -10237,16 +9960,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/srvx": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/srvx/-/srvx-0.8.9.tgz",
|
||||
@@ -10547,26 +10260,6 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/supabase": {
|
||||
"version": "2.101.0",
|
||||
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.101.0.tgz",
|
||||
"integrity": "sha512-9wqlvXGeI+VjOMUKOGEpcZzTJGLia6IOv8LWN3W2u8F6Xiv4ZqAuTuXCp3MWfkhV5x9KQaBtN5d6IiLEMWzkJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"supabase": "dist/supabase.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@supabase/cli-darwin-arm64": "2.101.0",
|
||||
"@supabase/cli-darwin-x64": "2.101.0",
|
||||
"@supabase/cli-linux-arm64": "2.101.0",
|
||||
"@supabase/cli-linux-arm64-musl": "2.101.0",
|
||||
"@supabase/cli-linux-x64": "2.101.0",
|
||||
"@supabase/cli-linux-x64-musl": "2.101.0",
|
||||
"@supabase/cli-windows-arm64": "2.101.0",
|
||||
"@supabase/cli-windows-x64": "2.101.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -11517,16 +11210,6 @@
|
||||
"node": ">= 6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"generate:sprites": "node scripts/generate-character-sprites.mjs"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.1",
|
||||
@@ -32,7 +31,6 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.1",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { PNG } from "pngjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const publicDir = path.join(root, "public");
|
||||
|
||||
const W = 32;
|
||||
const H = 48;
|
||||
|
||||
const RED = [255, 0, 0];
|
||||
const DARK_RED = [51, 0, 0];
|
||||
const WHITE = [255, 255, 255];
|
||||
const MAGENTA = [255, 0, 255];
|
||||
|
||||
function createCanvas() {
|
||||
return new PNG({ width: W, height: H });
|
||||
}
|
||||
|
||||
function setPixel(png, x, y, [r, g, b], alpha = 255) {
|
||||
if (x < 0 || y < 0 || x >= W || y >= H) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = (W * y + x) << 2;
|
||||
png.data[index] = r;
|
||||
png.data[index + 1] = g;
|
||||
png.data[index + 2] = b;
|
||||
png.data[index + 3] = alpha;
|
||||
}
|
||||
|
||||
function fillRect(png, x, y, width, height, color, alpha = 255) {
|
||||
for (let row = 0; row < height; row += 1) {
|
||||
for (let col = 0; col < width; col += 1) {
|
||||
setPixel(png, x + col, y + row, color, alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeSprite(relativePath, png) {
|
||||
const filePath = path.join(publicDir, relativePath);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, PNG.sync.write(png));
|
||||
console.log(`wrote ${relativePath}`);
|
||||
}
|
||||
|
||||
function drawSkin() {
|
||||
const png = createCanvas();
|
||||
fillRect(png, 11, 6, 10, 10, RED);
|
||||
fillRect(png, 10, 16, 12, 14, RED);
|
||||
fillRect(png, 8, 30, 6, 12, RED);
|
||||
fillRect(png, 18, 30, 6, 12, RED);
|
||||
fillRect(png, 6, 18, 4, 10, RED);
|
||||
fillRect(png, 22, 18, 4, 10, RED);
|
||||
fillRect(png, 12, 8, 8, 2, DARK_RED);
|
||||
writeSprite("character/skin/skin-1.png", png);
|
||||
}
|
||||
|
||||
function drawPantsVariant(index, widthOffset) {
|
||||
const png = createCanvas();
|
||||
fillRect(png, 8 + widthOffset, 28, 7 - widthOffset, 14, RED);
|
||||
fillRect(png, 17, 28, 7 - widthOffset, 14, RED);
|
||||
if (index === 2) {
|
||||
fillRect(png, 8, 26, 16, 4, RED);
|
||||
}
|
||||
if (index === 3) {
|
||||
fillRect(png, 7, 24, 18, 6, RED);
|
||||
}
|
||||
writeSprite(`character/pants/pants-${index}.png`, png);
|
||||
}
|
||||
|
||||
function drawShoesVariant(index) {
|
||||
const png = createCanvas();
|
||||
fillRect(png, 6, 40, 8, 4, RED);
|
||||
fillRect(png, 18, 40, 8, 4, RED);
|
||||
if (index >= 2) {
|
||||
fillRect(png, 5, 42, 10, 2, DARK_RED);
|
||||
fillRect(png, 17, 42, 10, 2, DARK_RED);
|
||||
}
|
||||
if (index === 3) {
|
||||
fillRect(png, 4, 38, 12, 3, RED);
|
||||
fillRect(png, 16, 38, 12, 3, RED);
|
||||
}
|
||||
writeSprite(`character/shoes/shoes-${index}.png`, png);
|
||||
}
|
||||
|
||||
function drawTorsoVariant(index) {
|
||||
const png = createCanvas();
|
||||
fillRect(png, 9, 16, 14, 14, RED);
|
||||
if (index >= 2) {
|
||||
fillRect(png, 7, 18, 3, 10, RED);
|
||||
fillRect(png, 22, 18, 3, 10, RED);
|
||||
}
|
||||
if (index >= 3) {
|
||||
fillRect(png, 11, 18, 10, 8, DARK_RED);
|
||||
}
|
||||
if (index === 4) {
|
||||
fillRect(png, 10, 14, 12, 4, RED);
|
||||
}
|
||||
writeSprite(`character/torso/torso-${index}.png`, png);
|
||||
}
|
||||
|
||||
function drawEyesVariant(index) {
|
||||
const png = createCanvas();
|
||||
fillRect(png, 12, 10, 3, 3, index === 1 ? RED : WHITE);
|
||||
fillRect(png, 17, 10, 3, 3, index === 1 ? RED : WHITE);
|
||||
fillRect(png, 13, 11, 1, 1, MAGENTA);
|
||||
fillRect(png, 18, 11, 1, 1, MAGENTA);
|
||||
if (index === 2) {
|
||||
fillRect(png, 11, 9, 5, 5, WHITE);
|
||||
fillRect(png, 16, 9, 5, 5, WHITE);
|
||||
fillRect(png, 13, 11, 2, 2, RED);
|
||||
fillRect(png, 18, 11, 2, 2, RED);
|
||||
}
|
||||
writeSprite(`character/eyes/eyes-${index}.png`, png);
|
||||
}
|
||||
|
||||
function drawHeadVariant(index) {
|
||||
const png = createCanvas();
|
||||
const y = index <= 3 ? 4 : 2;
|
||||
fillRect(png, 10, y, 12, index >= 5 ? 12 : 10, RED);
|
||||
if (index === 2) {
|
||||
fillRect(png, 8, y + 2, 3, 3, RED);
|
||||
fillRect(png, 21, y + 2, 3, 3, RED);
|
||||
}
|
||||
if (index === 3) {
|
||||
fillRect(png, 9, y - 1, 14, 3, RED);
|
||||
}
|
||||
if (index >= 4) {
|
||||
fillRect(png, 11, y + 1, 10, 4, DARK_RED);
|
||||
}
|
||||
if (index >= 6) {
|
||||
fillRect(png, 9, y - 2, 14, 4, RED);
|
||||
}
|
||||
if (index === 7) {
|
||||
fillRect(png, 7, y, 4, 8, RED);
|
||||
fillRect(png, 21, y, 4, 8, RED);
|
||||
}
|
||||
writeSprite(`character/head/head-${index}.png`, png);
|
||||
}
|
||||
|
||||
drawSkin();
|
||||
for (let index = 1; index <= 3; index += 1) {
|
||||
drawPantsVariant(index, index - 1);
|
||||
drawShoesVariant(index);
|
||||
}
|
||||
for (let index = 1; index <= 4; index += 1) {
|
||||
drawTorsoVariant(index);
|
||||
}
|
||||
for (let index = 1; index <= 2; index += 1) {
|
||||
drawEyesVariant(index);
|
||||
}
|
||||
for (let index = 1; index <= 7; index += 1) {
|
||||
drawHeadVariant(index);
|
||||
}
|
||||
|
||||
console.log("Character sprites generated.");
|
||||
Reference in New Issue
Block a user