feat(cortex-ui): pet sprite render with mood switcher

Cherry-pick 7 pixel-art sprites from feat/pet-ui-v1 (PR #16) — cat
(idle/happy/think/sleep), dog-idle, owl-idle, blob-idle — into
cortex-ui public dir so the PetEditor view renders a visual pet
instead of a bare JSON dump.

## Behavior

- Species inferred from first letter of pet_name:
  - 'd*' → dog, 'o*' → owl, 'b*' → blob, else → cat (default,
    has 4 mood states)
- Mood switcher: click idle / happy / think / sleep → swaps sprite
- image-rendering: pixelated for crisp pixel-art scaling
- 32×32 native scaled to 128×128 (4x) with nearest-neighbor

## Why now

User tested the UI end-to-end, confirmed auth+CORS+whitespace fix
works, then asked for the cat. The sprite-gen commit (PR #16) is
still unmerged but sprites are sibling static assets — safe to copy
into cortex-ui without blocking on PR merge. Ownership stays with
the sprite-gen branch; cortex-ui just embeds the artefacts.

Rebuild hash: index-RLWTBoLo.js + index-BzERxlis.css.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Parfii-bot 2026-04-24 03:17:25 +08:00
parent 39f95f7e04
commit a42847063f
9 changed files with 92 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -14,6 +14,30 @@
let manifest = $state<PetManifest | null>(null);
let error = $state<string | null>(null);
let loading = $state(true);
let mood = $state<'idle' | 'happy' | 'think' | 'sleep'>('idle');
const AVAILABLE = {
cat: ['idle', 'happy', 'think', 'sleep'] as const,
dog: ['idle'] as const,
owl: ['idle'] as const,
blob: ['idle'] as const,
};
// Pick species from pet name first letter, defaulting to cat (most states available)
function species_for(pet_name: string): 'cat' | 'dog' | 'owl' | 'blob' {
const first = pet_name.trim().toLowerCase().charAt(0);
if (first === 'd') return 'dog';
if (first === 'o') return 'owl';
if (first === 'b') return 'blob';
return 'cat';
}
function sprite_src(pet_name: string, m: typeof mood): string {
const sp = species_for(pet_name);
const states = AVAILABLE[sp] as readonly string[];
const state = states.includes(m) ? m : 'idle';
return `./sprites/32px/${sp}-${state}.png`;
}
onMount(async () => {
if (!user_id) {
@ -34,6 +58,28 @@
<h2>Pet: {user_id}</h2>
{#if manifest}
<div class="pet-sprite-box">
<img
class="pet-sprite"
src={sprite_src(manifest.identity.pet_name, mood)}
alt="{manifest.identity.pet_name} ({mood})"
width="128"
height="128"
/>
<div class="pet-sprite-name">{manifest.identity.pet_name}</div>
<div class="pet-sprite-moods">
{#each AVAILABLE[species_for(manifest.identity.pet_name)] as m}
<button
class="mood-btn"
class:active={mood === m}
onclick={() => (mood = m as typeof mood)}
>{m}</button>
{/each}
</div>
</div>
{/if}
{#if loading}
<p class="muted">Loading manifest…</p>
{:else if error}

View file

@ -184,3 +184,49 @@ pre {
overflow-x: auto;
font-size: 13px;
}
.pet-sprite-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin: 16px 0 24px;
padding: 20px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 8px;
}
.pet-sprite {
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
-ms-interpolation-mode: nearest-neighbor;
width: 128px;
height: 128px;
display: block;
}
.pet-sprite-name {
font-weight: 600;
letter-spacing: 0.02em;
}
.pet-sprite-moods {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.mood-btn {
padding: 4px 10px;
font-size: 12px;
background: transparent;
border: 1px solid var(--border);
color: var(--text);
border-radius: 4px;
cursor: pointer;
}
.mood-btn.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.mood-btn:hover:not(.active) {
border-color: var(--accent);
}