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>
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
BIN
_ts_packages/packages/cortex-ui/public/sprites/32px/cat-idle.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
BIN
_ts_packages/packages/cortex-ui/public/sprites/32px/dog-idle.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
_ts_packages/packages/cortex-ui/public/sprites/32px/owl-idle.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||